diff --git a/Cargo.lock b/Cargo.lock index 3b070c56e5..e1887d273f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5,23 +5,6 @@ version = 3 [[package]] name = "activity_indicator" version = "0.1.0" -dependencies = [ - "auto_update", - "editor", - "futures 0.3.28", - "gpui", - "language", - "project", - "settings", - "smallvec", - "theme", - "util", - "workspace", -] - -[[package]] -name = "activity_indicator2" -version = "0.1.0" dependencies = [ "anyhow", "auto_update2", @@ -1128,23 +1111,6 @@ dependencies = [ [[package]] name = "breadcrumbs" version = "0.1.0" -dependencies = [ - "collections", - "editor", - "gpui", - "itertools 0.10.5", - "language", - "outline", - "project", - "search", - "settings", - "theme", - "workspace", -] - -[[package]] -name = "breadcrumbs2" -version = "0.1.0" dependencies = [ "collections", "editor2", @@ -1855,7 +1821,7 @@ dependencies = [ "postage", "pretty_assertions", "project2", - "recent_projects2", + "recent_projects", "rich_text2", "rpc2", "schemars", @@ -2059,25 +2025,6 @@ dependencies = [ [[package]] name = "copilot_button" version = "0.1.0" -dependencies = [ - "anyhow", - "context_menu", - "copilot", - "editor", - "fs", - "futures 0.3.28", - "gpui", - "language", - "settings", - "smol", - "theme", - "util", - "workspace", -] - -[[package]] -name = "copilot_button2" -version = "0.1.0" dependencies = [ "anyhow", "copilot2", @@ -4593,23 +4540,6 @@ dependencies = [ [[package]] name = "language_selector" version = "0.1.0" -dependencies = [ - "anyhow", - "editor", - "fuzzy", - "gpui", - "language", - "picker", - "project", - "settings", - "theme", - "util", - "workspace", -] - -[[package]] -name = "language_selector2" -version = "0.1.0" dependencies = [ "anyhow", "editor2", @@ -6618,35 +6548,6 @@ dependencies = [ [[package]] name = "project_panel" version = "0.1.0" -dependencies = [ - "anyhow", - "client", - "collections", - "context_menu", - "db", - "drag_and_drop", - "editor", - "futures 0.3.28", - "gpui", - "language", - "menu", - "postage", - "pretty_assertions", - "project", - "schemars", - "serde", - "serde_derive", - "serde_json", - "settings", - "theme", - "unicase", - "util", - "workspace", -] - -[[package]] -name = "project_panel2" -version = "0.1.0" dependencies = [ "anyhow", "client2", @@ -7029,27 +6930,6 @@ dependencies = [ [[package]] name = "recent_projects" version = "0.1.0" -dependencies = [ - "db", - "editor", - "futures 0.3.28", - "fuzzy", - "gpui", - "language", - "ordered-float 2.10.0", - "picker", - "postage", - "settings", - "smol", - "text", - "theme", - "util", - "workspace", -] - -[[package]] -name = "recent_projects2" -version = "0.1.0" dependencies = [ "editor2", "futures 0.3.28", @@ -11290,7 +11170,7 @@ dependencies = [ name = "zed" version = "0.119.0" dependencies = [ - "activity_indicator2", + "activity_indicator", "ai2", "anyhow", "assistant2", @@ -11301,7 +11181,7 @@ dependencies = [ "audio2", "auto_update2", "backtrace", - "breadcrumbs2", + "breadcrumbs", "call2", "channel2", "chrono", @@ -11311,7 +11191,7 @@ dependencies = [ "collections", "command_palette", "copilot2", - "copilot_button2", + "copilot_button", "ctor", "db2", "diagnostics", @@ -11332,7 +11212,7 @@ dependencies = [ "isahc", "journal2", "language2", - "language_selector2", + "language_selector", "language_tools2", "lazy_static", "libc", @@ -11346,11 +11226,11 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project2", - "project_panel2", + "project_panel", "project_symbols", "quick_action_bar", "rand 0.8.5", - "recent_projects2", + "recent_projects", "regex", "rope2", "rpc2", diff --git a/Cargo.toml b/Cargo.toml index b117e09998..5f35f8b4fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ "crates/activity_indicator", - "crates/activity_indicator2", "crates/ai", "crates/assistant", "crates/assistant2", @@ -10,7 +9,6 @@ members = [ "crates/auto_update", "crates/auto_update2", "crates/breadcrumbs", - "crates/breadcrumbs2", "crates/call", "crates/call2", "crates/channel", @@ -58,7 +56,6 @@ members = [ "crates/language", "crates/language2", "crates/language_selector", - "crates/language_selector2", "crates/language_tools", "crates/language_tools2", "crates/live_kit_client", @@ -84,11 +81,9 @@ members = [ "crates/project", "crates/project2", "crates/project_panel", - "crates/project_panel2", "crates/project_symbols", "crates/quick_action_bar", "crates/recent_projects", - "crates/recent_projects2", "crates/rope", "crates/rpc", "crates/rpc2", diff --git a/crates/activity_indicator/Cargo.toml b/crates/activity_indicator/Cargo.toml index 43d16e6b9b..c9921ffcce 100644 --- a/crates/activity_indicator/Cargo.toml +++ b/crates/activity_indicator/Cargo.toml @@ -9,18 +9,20 @@ path = "src/activity_indicator.rs" doctest = false [dependencies] -auto_update = { path = "../auto_update" } -editor = { path = "../editor" } -language = { path = "../language" } -gpui = { path = "../gpui" } -project = { path = "../project" } -settings = { path = "../settings" } +auto_update = { path = "../auto_update2", package = "auto_update2" } +editor = { path = "../editor2", package = "editor2" } +language = { path = "../language2", package = "language2" } +gpui = { path = "../gpui2", package = "gpui2" } +project = { path = "../project2", package = "project2" } +settings = { path = "../settings2", package = "settings2" } +ui = { path = "../ui2", package = "ui2" } util = { path = "../util" } -theme = { path = "../theme" } -workspace = { path = "../workspace" } +theme = { path = "../theme2", package = "theme2" } +workspace = { path = "../workspace2", package = "workspace2" } +anyhow.workspace = true futures.workspace = true smallvec.workspace = true [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } diff --git a/crates/activity_indicator/src/activity_indicator.rs b/crates/activity_indicator/src/activity_indicator.rs index f9b34add9a..4b990fa430 100644 --- a/crates/activity_indicator/src/activity_indicator.rs +++ b/crates/activity_indicator/src/activity_indicator.rs @@ -2,19 +2,19 @@ use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; use editor::Editor; use futures::StreamExt; use gpui::{ - actions, anyhow, - elements::*, - platform::{CursorStyle, MouseButton}, - AppContext, Entity, ModelHandle, View, ViewContext, ViewHandle, + actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model, + ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, + ViewContext, VisualContext as _, }; use language::{LanguageRegistry, LanguageServerBinaryStatus}; use project::{LanguageServerProgress, Project}; use smallvec::SmallVec; use std::{cmp::Reverse, fmt::Write, sync::Arc}; +use ui::prelude::*; use util::ResultExt; use workspace::{item::ItemHandle, StatusItemView, Workspace}; -actions!(lsp_status, [ShowErrorMessage]); +actions!(activity_indicator, [ShowErrorMessage]); const DOWNLOAD_ICON: &str = "icons/download.svg"; const WARNING_ICON: &str = "icons/warning.svg"; @@ -25,8 +25,8 @@ pub enum Event { pub struct ActivityIndicator { statuses: Vec, - project: ModelHandle, - auto_updater: Option>, + project: Model, + auto_updater: Option>, } struct LspStatus { @@ -47,20 +47,15 @@ struct Content { on_click: Option)>>, } -pub fn init(cx: &mut AppContext) { - cx.add_action(ActivityIndicator::show_error_message); - cx.add_action(ActivityIndicator::dismiss_error_message); -} - impl ActivityIndicator { pub fn new( workspace: &mut Workspace, languages: Arc, cx: &mut ViewContext, - ) -> ViewHandle { + ) -> View { let project = workspace.project().clone(); let auto_updater = AutoUpdater::get(cx); - let this = cx.add_view(|cx: &mut ViewContext| { + let this = cx.new_view(|cx: &mut ViewContext| { let mut status_events = languages.language_server_binary_statuses(); cx.spawn(|this, mut cx| async move { while let Some((language, event)) = status_events.next().await { @@ -77,11 +72,13 @@ impl ActivityIndicator { }) .detach(); cx.observe(&project, |_, _, cx| cx.notify()).detach(); + if let Some(auto_updater) = auto_updater.as_ref() { cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); } - cx.observe_active_labeled_tasks(|_, cx| cx.notify()) - .detach(); + + // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) + // .detach(); Self { statuses: Default::default(), @@ -89,6 +86,7 @@ impl ActivityIndicator { auto_updater, } }); + cx.subscribe(&this, move |workspace, _, event, cx| match event { Event::ShowError { lsp_name, error } => { if let Some(buffer) = project @@ -104,7 +102,7 @@ impl ActivityIndicator { }); workspace.add_item( Box::new( - cx.add_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), + cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), ), cx, ); @@ -290,71 +288,41 @@ impl ActivityIndicator { }; } - if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { - return Content { - icon: None, - message: most_recent_active_task.to_string(), - on_click: None, - }; - } + // todo!(show active tasks) + // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { + // return Content { + // icon: None, + // message: most_recent_active_task.to_string(), + // on_click: None, + // }; + // } Default::default() } } -impl Entity for ActivityIndicator { - type Event = Event; -} +impl EventEmitter for ActivityIndicator {} -impl View for ActivityIndicator { - fn ui_name() -> &'static str { - "ActivityIndicator" - } +impl Render for ActivityIndicator { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let content = self.content_to_render(cx); - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let Content { - icon, - message, - on_click, - } = self.content_to_render(cx); + let mut result = h_stack() + .id("activity-indicator") + .on_action(cx.listener(Self::show_error_message)) + .on_action(cx.listener(Self::dismiss_error_message)); - let mut element = MouseEventHandler::new::(0, cx, |state, cx| { - let theme = &theme::current(cx).workspace.status_bar.lsp_status; - let style = if state.hovered() && on_click.is_some() { - theme.hovered.as_ref().unwrap_or(&theme.default) - } else { - &theme.default - }; - Flex::row() - .with_children(icon.map(|path| { - Svg::new(path) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_width) - .contained() - .with_margin_right(style.icon_spacing) - .aligned() - .into_any_named("activity-icon") + if let Some(on_click) = content.on_click { + result = result + .cursor(CursorStyle::PointingHand) + .on_click(cx.listener(move |this, _, cx| { + on_click(this, cx); })) - .with_child( - Text::new(message, style.message.clone()) - .with_soft_wrap(false) - .aligned(), - ) - .constrained() - .with_height(style.height) - .contained() - .with_style(style.container) - .aligned() - }); - - if let Some(on_click) = on_click.clone() { - element = element - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx)); } - element.into_any() + result + .children(content.icon.map(|icon| svg().path(icon))) + .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small)) } } diff --git a/crates/activity_indicator2/Cargo.toml b/crates/activity_indicator2/Cargo.toml deleted file mode 100644 index 400869d2fd..0000000000 --- a/crates/activity_indicator2/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "activity_indicator2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/activity_indicator.rs" -doctest = false - -[dependencies] -auto_update = { path = "../auto_update2", package = "auto_update2" } -editor = { path = "../editor2", package = "editor2" } -language = { path = "../language2", package = "language2" } -gpui = { path = "../gpui2", package = "gpui2" } -project = { path = "../project2", package = "project2" } -settings = { path = "../settings2", package = "settings2" } -ui = { path = "../ui2", package = "ui2" } -util = { path = "../util" } -theme = { path = "../theme2", package = "theme2" } -workspace = { path = "../workspace2", package = "workspace2" } - -anyhow.workspace = true -futures.workspace = true -smallvec.workspace = true - -[dev-dependencies] -editor = { path = "../editor2", package = "editor2", features = ["test-support"] } diff --git a/crates/activity_indicator2/src/activity_indicator.rs b/crates/activity_indicator2/src/activity_indicator.rs deleted file mode 100644 index 4b990fa430..0000000000 --- a/crates/activity_indicator2/src/activity_indicator.rs +++ /dev/null @@ -1,331 +0,0 @@ -use auto_update::{AutoUpdateStatus, AutoUpdater, DismissErrorMessage}; -use editor::Editor; -use futures::StreamExt; -use gpui::{ - actions, svg, AppContext, CursorStyle, EventEmitter, InteractiveElement as _, Model, - ParentElement as _, Render, SharedString, StatefulInteractiveElement, Styled, View, - ViewContext, VisualContext as _, -}; -use language::{LanguageRegistry, LanguageServerBinaryStatus}; -use project::{LanguageServerProgress, Project}; -use smallvec::SmallVec; -use std::{cmp::Reverse, fmt::Write, sync::Arc}; -use ui::prelude::*; -use util::ResultExt; -use workspace::{item::ItemHandle, StatusItemView, Workspace}; - -actions!(activity_indicator, [ShowErrorMessage]); - -const DOWNLOAD_ICON: &str = "icons/download.svg"; -const WARNING_ICON: &str = "icons/warning.svg"; - -pub enum Event { - ShowError { lsp_name: Arc, error: String }, -} - -pub struct ActivityIndicator { - statuses: Vec, - project: Model, - auto_updater: Option>, -} - -struct LspStatus { - name: Arc, - status: LanguageServerBinaryStatus, -} - -struct PendingWork<'a> { - language_server_name: &'a str, - progress_token: &'a str, - progress: &'a LanguageServerProgress, -} - -#[derive(Default)] -struct Content { - icon: Option<&'static str>, - message: String, - on_click: Option)>>, -} - -impl ActivityIndicator { - pub fn new( - workspace: &mut Workspace, - languages: Arc, - cx: &mut ViewContext, - ) -> View { - let project = workspace.project().clone(); - let auto_updater = AutoUpdater::get(cx); - let this = cx.new_view(|cx: &mut ViewContext| { - let mut status_events = languages.language_server_binary_statuses(); - cx.spawn(|this, mut cx| async move { - while let Some((language, event)) = status_events.next().await { - this.update(&mut cx, |this, cx| { - this.statuses.retain(|s| s.name != language.name()); - this.statuses.push(LspStatus { - name: language.name(), - status: event, - }); - cx.notify(); - })?; - } - anyhow::Ok(()) - }) - .detach(); - cx.observe(&project, |_, _, cx| cx.notify()).detach(); - - if let Some(auto_updater) = auto_updater.as_ref() { - cx.observe(auto_updater, |_, _, cx| cx.notify()).detach(); - } - - // cx.observe_active_labeled_tasks(|_, cx| cx.notify()) - // .detach(); - - Self { - statuses: Default::default(), - project: project.clone(), - auto_updater, - } - }); - - cx.subscribe(&this, move |workspace, _, event, cx| match event { - Event::ShowError { lsp_name, error } => { - if let Some(buffer) = project - .update(cx, |project, cx| project.create_buffer(error, None, cx)) - .log_err() - { - buffer.update(cx, |buffer, cx| { - buffer.edit( - [(0..0, format!("Language server error: {}\n\n", lsp_name))], - None, - cx, - ); - }); - workspace.add_item( - Box::new( - cx.new_view(|cx| Editor::for_buffer(buffer, Some(project.clone()), cx)), - ), - cx, - ); - } - } - }) - .detach(); - this - } - - fn show_error_message(&mut self, _: &ShowErrorMessage, cx: &mut ViewContext) { - self.statuses.retain(|status| { - if let LanguageServerBinaryStatus::Failed { error } = &status.status { - cx.emit(Event::ShowError { - lsp_name: status.name.clone(), - error: error.clone(), - }); - false - } else { - true - } - }); - - cx.notify(); - } - - fn dismiss_error_message(&mut self, _: &DismissErrorMessage, cx: &mut ViewContext) { - if let Some(updater) = &self.auto_updater { - updater.update(cx, |updater, cx| { - updater.dismiss_error(cx); - }); - } - cx.notify(); - } - - fn pending_language_server_work<'a>( - &self, - cx: &'a AppContext, - ) -> impl Iterator> { - self.project - .read(cx) - .language_server_statuses() - .rev() - .filter_map(|status| { - if status.pending_work.is_empty() { - None - } else { - let mut pending_work = status - .pending_work - .iter() - .map(|(token, progress)| PendingWork { - language_server_name: status.name.as_str(), - progress_token: token.as_str(), - progress, - }) - .collect::>(); - pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at)); - Some(pending_work) - } - }) - .flatten() - } - - fn content_to_render(&mut self, cx: &mut ViewContext) -> Content { - // Show any language server has pending activity. - let mut pending_work = self.pending_language_server_work(cx); - if let Some(PendingWork { - language_server_name, - progress_token, - progress, - }) = pending_work.next() - { - let mut message = language_server_name.to_string(); - - message.push_str(": "); - if let Some(progress_message) = progress.message.as_ref() { - message.push_str(progress_message); - } else { - message.push_str(progress_token); - } - - if let Some(percentage) = progress.percentage { - write!(&mut message, " ({}%)", percentage).unwrap(); - } - - let additional_work_count = pending_work.count(); - if additional_work_count > 0 { - write!(&mut message, " + {} more", additional_work_count).unwrap(); - } - - return Content { - icon: None, - message, - on_click: None, - }; - } - - // Show any language server installation info. - let mut downloading = SmallVec::<[_; 3]>::new(); - let mut checking_for_update = SmallVec::<[_; 3]>::new(); - let mut failed = SmallVec::<[_; 3]>::new(); - for status in &self.statuses { - let name = status.name.clone(); - match status.status { - LanguageServerBinaryStatus::CheckingForUpdate => checking_for_update.push(name), - LanguageServerBinaryStatus::Downloading => downloading.push(name), - LanguageServerBinaryStatus::Failed { .. } => failed.push(name), - LanguageServerBinaryStatus::Downloaded | LanguageServerBinaryStatus::Cached => {} - } - } - - if !downloading.is_empty() { - return Content { - icon: Some(DOWNLOAD_ICON), - message: format!( - "Downloading {} language server{}...", - downloading.join(", "), - if downloading.len() > 1 { "s" } else { "" } - ), - on_click: None, - }; - } else if !checking_for_update.is_empty() { - return Content { - icon: Some(DOWNLOAD_ICON), - message: format!( - "Checking for updates to {} language server{}...", - checking_for_update.join(", "), - if checking_for_update.len() > 1 { - "s" - } else { - "" - } - ), - on_click: None, - }; - } else if !failed.is_empty() { - return Content { - icon: Some(WARNING_ICON), - message: format!( - "Failed to download {} language server{}. Click to show error.", - failed.join(", "), - if failed.len() > 1 { "s" } else { "" } - ), - on_click: Some(Arc::new(|this, cx| { - this.show_error_message(&Default::default(), cx) - })), - }; - } - - // Show any application auto-update info. - if let Some(updater) = &self.auto_updater { - return match &updater.read(cx).status() { - AutoUpdateStatus::Checking => Content { - icon: Some(DOWNLOAD_ICON), - message: "Checking for Zed updates…".to_string(), - on_click: None, - }, - AutoUpdateStatus::Downloading => Content { - icon: Some(DOWNLOAD_ICON), - message: "Downloading Zed update…".to_string(), - on_click: None, - }, - AutoUpdateStatus::Installing => Content { - icon: Some(DOWNLOAD_ICON), - message: "Installing Zed update…".to_string(), - on_click: None, - }, - AutoUpdateStatus::Updated => Content { - icon: None, - message: "Click to restart and update Zed".to_string(), - on_click: Some(Arc::new(|_, cx| { - workspace::restart(&Default::default(), cx) - })), - }, - AutoUpdateStatus::Errored => Content { - icon: Some(WARNING_ICON), - message: "Auto update failed".to_string(), - on_click: Some(Arc::new(|this, cx| { - this.dismiss_error_message(&Default::default(), cx) - })), - }, - AutoUpdateStatus::Idle => Default::default(), - }; - } - - // todo!(show active tasks) - // if let Some(most_recent_active_task) = cx.active_labeled_tasks().last() { - // return Content { - // icon: None, - // message: most_recent_active_task.to_string(), - // on_click: None, - // }; - // } - - Default::default() - } -} - -impl EventEmitter for ActivityIndicator {} - -impl Render for ActivityIndicator { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let content = self.content_to_render(cx); - - let mut result = h_stack() - .id("activity-indicator") - .on_action(cx.listener(Self::show_error_message)) - .on_action(cx.listener(Self::dismiss_error_message)); - - if let Some(on_click) = content.on_click { - result = result - .cursor(CursorStyle::PointingHand) - .on_click(cx.listener(move |this, _, cx| { - on_click(this, cx); - })) - } - - result - .children(content.icon.map(|icon| svg().path(icon))) - .child(Label::new(SharedString::from(content.message)).size(LabelSize::Small)) - } -} - -impl StatusItemView for ActivityIndicator { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, _: &mut ViewContext) {} -} diff --git a/crates/breadcrumbs/Cargo.toml b/crates/breadcrumbs/Cargo.toml index 412a79a317..bca907ab15 100644 --- a/crates/breadcrumbs/Cargo.toml +++ b/crates/breadcrumbs/Cargo.toml @@ -10,18 +10,19 @@ doctest = false [dependencies] collections = { path = "../collections" } -editor = { path = "../editor" } -gpui = { path = "../gpui" } -language = { path = "../language" } -project = { path = "../project" } -search = { path = "../search" } -settings = { path = "../settings" } -theme = { path = "../theme" } -workspace = { path = "../workspace" } -outline = { path = "../outline" } +editor = { package = "editor2", path = "../editor2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +language = { package = "language2", path = "../language2" } +project = { package = "project2", path = "../project2" } +search = { package = "search2", path = "../search2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +workspace = { package = "workspace2", path = "../workspace2" } +outline = { package = "outline2", path = "../outline2" } itertools = "0.10" [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } +gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } +workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/breadcrumbs/src/breadcrumbs.rs b/crates/breadcrumbs/src/breadcrumbs.rs index 41985edb75..2e4306f0bc 100644 --- a/crates/breadcrumbs/src/breadcrumbs.rs +++ b/crates/breadcrumbs/src/breadcrumbs.rs @@ -1,108 +1,74 @@ +use editor::Editor; use gpui::{ - elements::*, platform::MouseButton, AppContext, Entity, Subscription, View, ViewContext, - ViewHandle, WeakViewHandle, + Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, + ViewContext, }; use itertools::Itertools; -use search::ProjectSearchView; +use theme::ActiveTheme; +use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip}; use workspace::{ item::{ItemEvent, ItemHandle}, - ToolbarItemLocation, ToolbarItemView, Workspace, + ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, }; -pub enum Event { - UpdateLocation, -} - pub struct Breadcrumbs { pane_focused: bool, active_item: Option>, - project_search: Option>, subscription: Option, - workspace: WeakViewHandle, } impl Breadcrumbs { - pub fn new(workspace: &Workspace) -> Self { + pub fn new() -> Self { Self { pane_focused: false, active_item: Default::default(), subscription: Default::default(), - project_search: Default::default(), - workspace: workspace.weak_handle(), } } } -impl Entity for Breadcrumbs { - type Event = Event; -} +impl EventEmitter for Breadcrumbs {} -impl View for Breadcrumbs { - fn ui_name() -> &'static str { - "Breadcrumbs" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let active_item = match &self.active_item { - Some(active_item) => active_item, - None => return Empty::new().into_any(), +impl Render for Breadcrumbs { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let element = h_stack().text_ui(); + let Some(active_item) = self.active_item.as_ref() else { + return element; + }; + let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { + return element; }; - let not_editor = active_item.downcast::().is_none(); - let theme = theme::current(cx).clone(); - let style = &theme.workspace.toolbar.breadcrumbs; + let highlighted_segments = segments.into_iter().map(|segment| { + let mut text_style = cx.text_style(); + text_style.color = Color::Muted.color(cx); - let breadcrumbs = match active_item.breadcrumbs(&theme, cx) { - Some(breadcrumbs) => breadcrumbs, - None => return Empty::new().into_any(), - } - .into_iter() - .map(|breadcrumb| { - Text::new( - breadcrumb.text, - theme.workspace.toolbar.breadcrumbs.default.text.clone(), - ) - .with_highlights(breadcrumb.highlights.unwrap_or_default()) - .into_any() + StyledText::new(segment.text) + .with_highlights(&text_style, segment.highlights.unwrap_or_default()) + .into_any() + }); + let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { + Label::new("›").color(Color::Muted).into_any_element() }); - let crumbs = Flex::row() - .with_children(Itertools::intersperse_with(breadcrumbs, || { - Label::new(" › ", style.default.text.clone()).into_any() - })) - .constrained() - .with_height(theme.workspace.toolbar.breadcrumb_height) - .contained(); - - if not_editor || !self.pane_focused { - return crumbs - .with_style(style.default.container) - .aligned() - .left() - .into_any(); + let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs); + match active_item + .downcast::() + .map(|editor| editor.downgrade()) + { + Some(editor) => element.child( + ButtonLike::new("toggle outline view") + .child(breadcrumbs_stack) + .style(ButtonStyle::Subtle) + .on_click(move |_, cx| { + if let Some(editor) = editor.upgrade() { + outline::toggle(editor, &outline::Toggle, cx) + } + }) + .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), + ), + None => element.child(breadcrumbs_stack), } - - MouseEventHandler::new::(0, cx, |state, _| { - let style = style.style_for(state); - crumbs.with_style(style.container) - }) - .on_click(MouseButton::Left, |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - outline::toggle(workspace, &Default::default(), cx) - }) - } - }) - .with_tooltip::( - 0, - "Show symbol outline".to_owned(), - Some(Box::new(outline::Toggle)), - theme.tooltip.clone(), - cx, - ) - .aligned() - .left() - .into_any() } } @@ -114,19 +80,21 @@ impl ToolbarItemView for Breadcrumbs { ) -> ToolbarItemLocation { cx.notify(); self.active_item = None; - self.project_search = None; if let Some(item) = active_pane_item { - let this = cx.weak_handle(); + let this = cx.view().downgrade(); self.subscription = Some(item.subscribe_to_item_events( cx, Box::new(move |event, cx| { - if let Some(this) = this.upgrade(cx) { - if let ItemEvent::UpdateBreadcrumbs = event { - this.update(cx, |_, cx| { - cx.emit(Event::UpdateLocation); - cx.notify(); - }); - } + if let ItemEvent::UpdateBreadcrumbs = event { + this.update(cx, |this, cx| { + cx.notify(); + if let Some(active_item) = this.active_item.as_ref() { + cx.emit(ToolbarItemEvent::ChangeLocation( + active_item.breadcrumb_location(cx), + )) + } + }) + .ok(); } }), )); @@ -137,19 +105,6 @@ impl ToolbarItemView for Breadcrumbs { } } - fn location_for_event( - &self, - _: &Event, - current_location: ToolbarItemLocation, - cx: &AppContext, - ) -> ToolbarItemLocation { - if let Some(active_item) = self.active_item.as_ref() { - active_item.breadcrumb_location(cx) - } else { - current_location - } - } - fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext) { self.pane_focused = pane_focused; } diff --git a/crates/breadcrumbs2/Cargo.toml b/crates/breadcrumbs2/Cargo.toml deleted file mode 100644 index de3a54d40e..0000000000 --- a/crates/breadcrumbs2/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "breadcrumbs2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/breadcrumbs.rs" -doctest = false - -[dependencies] -collections = { path = "../collections" } -editor = { package = "editor2", path = "../editor2" } -gpui = { package = "gpui2", path = "../gpui2" } -ui = { package = "ui2", path = "../ui2" } -language = { package = "language2", path = "../language2" } -project = { package = "project2", path = "../project2" } -search = { package = "search2", path = "../search2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -workspace = { package = "workspace2", path = "../workspace2" } -outline = { package = "outline2", path = "../outline2" } -itertools = "0.10" - -[dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } -gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] } -workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] } diff --git a/crates/breadcrumbs2/src/breadcrumbs.rs b/crates/breadcrumbs2/src/breadcrumbs.rs deleted file mode 100644 index 2e4306f0bc..0000000000 --- a/crates/breadcrumbs2/src/breadcrumbs.rs +++ /dev/null @@ -1,111 +0,0 @@ -use editor::Editor; -use gpui::{ - Element, EventEmitter, IntoElement, ParentElement, Render, StyledText, Subscription, - ViewContext, -}; -use itertools::Itertools; -use theme::ActiveTheme; -use ui::{prelude::*, ButtonLike, ButtonStyle, Label, Tooltip}; -use workspace::{ - item::{ItemEvent, ItemHandle}, - ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, -}; - -pub struct Breadcrumbs { - pane_focused: bool, - active_item: Option>, - subscription: Option, -} - -impl Breadcrumbs { - pub fn new() -> Self { - Self { - pane_focused: false, - active_item: Default::default(), - subscription: Default::default(), - } - } -} - -impl EventEmitter for Breadcrumbs {} - -impl Render for Breadcrumbs { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let element = h_stack().text_ui(); - let Some(active_item) = self.active_item.as_ref() else { - return element; - }; - let Some(segments) = active_item.breadcrumbs(cx.theme(), cx) else { - return element; - }; - - let highlighted_segments = segments.into_iter().map(|segment| { - let mut text_style = cx.text_style(); - text_style.color = Color::Muted.color(cx); - - StyledText::new(segment.text) - .with_highlights(&text_style, segment.highlights.unwrap_or_default()) - .into_any() - }); - let breadcrumbs = Itertools::intersperse_with(highlighted_segments, || { - Label::new("›").color(Color::Muted).into_any_element() - }); - - let breadcrumbs_stack = h_stack().gap_1().children(breadcrumbs); - match active_item - .downcast::() - .map(|editor| editor.downgrade()) - { - Some(editor) => element.child( - ButtonLike::new("toggle outline view") - .child(breadcrumbs_stack) - .style(ButtonStyle::Subtle) - .on_click(move |_, cx| { - if let Some(editor) = editor.upgrade() { - outline::toggle(editor, &outline::Toggle, cx) - } - }) - .tooltip(|cx| Tooltip::for_action("Show symbol outline", &outline::Toggle, cx)), - ), - None => element.child(breadcrumbs_stack), - } - } -} - -impl ToolbarItemView for Breadcrumbs { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) -> ToolbarItemLocation { - cx.notify(); - self.active_item = None; - if let Some(item) = active_pane_item { - let this = cx.view().downgrade(); - self.subscription = Some(item.subscribe_to_item_events( - cx, - Box::new(move |event, cx| { - if let ItemEvent::UpdateBreadcrumbs = event { - this.update(cx, |this, cx| { - cx.notify(); - if let Some(active_item) = this.active_item.as_ref() { - cx.emit(ToolbarItemEvent::ChangeLocation( - active_item.breadcrumb_location(cx), - )) - } - }) - .ok(); - } - }), - )); - self.active_item = Some(item.boxed_clone()); - item.breadcrumb_location(cx) - } else { - ToolbarItemLocation::Hidden - } - } - - fn pane_focus_update(&mut self, pane_focused: bool, _: &mut ViewContext) { - self.pane_focused = pane_focused; - } -} diff --git a/crates/collab_ui/Cargo.toml b/crates/collab_ui/Cargo.toml index 5f2f85c48f..a71cecfeea 100644 --- a/crates/collab_ui/Cargo.toml +++ b/crates/collab_ui/Cargo.toml @@ -41,7 +41,7 @@ notifications = { package = "notifications2", path = "../notifications2" } rich_text = { package = "rich_text2", path = "../rich_text2" } picker = { package = "picker2", path = "../picker2" } project = { package = "project2", path = "../project2" } -recent_projects = { package = "recent_projects2", path = "../recent_projects2" } +recent_projects = { path = "../recent_projects" } rpc = { package ="rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" } feature_flags = { package = "feature_flags2", path = "../feature_flags2"} diff --git a/crates/copilot_button/Cargo.toml b/crates/copilot_button/Cargo.toml index c93e1920dc..f7611ed3f3 100644 --- a/crates/copilot_button/Cargo.toml +++ b/crates/copilot_button/Cargo.toml @@ -9,19 +9,19 @@ path = "src/copilot_button.rs" doctest = false [dependencies] -copilot = { path = "../copilot" } -editor = { path = "../editor" } -fs = { path = "../fs" } -context_menu = { path = "../context_menu" } -gpui = { path = "../gpui" } -language = { path = "../language" } -settings = { path = "../settings" } -theme = { path = "../theme" } +copilot = { package = "copilot2", path = "../copilot2" } +editor = { package = "editor2", path = "../editor2" } +fs = { package = "fs2", path = "../fs2" } +zed-actions = { package="zed_actions2", path = "../zed_actions2"} +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } util = { path = "../util" } -workspace = { path = "../workspace" } +workspace = { package = "workspace2", path = "../workspace2" } anyhow.workspace = true smol.workspace = true futures.workspace = true [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index ce0f364806..60b25fee12 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -1,32 +1,31 @@ use anyhow::Result; -use context_menu::{ContextMenu, ContextMenuItem}; use copilot::{Copilot, SignOut, Status}; use editor::{scroll::autoscroll::Autoscroll, Editor}; use fs::Fs; use gpui::{ - elements::*, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, AsyncAppContext, Element, Entity, MouseState, Subscription, View, - ViewContext, ViewHandle, WeakViewHandle, WindowContext, + div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement, + Render, Subscription, View, ViewContext, WeakView, WindowContext, }; use language::{ language_settings::{self, all_language_settings, AllLanguageSettings}, File, Language, }; -use settings::{update_settings_file, SettingsStore}; +use settings::{update_settings_file, Settings, SettingsStore}; use std::{path::Path, sync::Arc}; use util::{paths, ResultExt}; use workspace::{ - create_and_open_local_file, item::ItemHandle, - notifications::simple_message_notification::OsOpen, StatusItemView, Toast, Workspace, + create_and_open_local_file, + item::ItemHandle, + ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip}, + StatusItemView, Toast, Workspace, }; +use zed_actions::OpenBrowser; const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; const COPILOT_STARTING_TOAST_ID: usize = 1337; const COPILOT_ERROR_TOAST_ID: usize = 1338; pub struct CopilotButton { - popup_menu: ViewHandle, editor_subscription: Option<(Subscription, usize)>, editor_enabled: Option, language: Option>, @@ -34,25 +33,15 @@ pub struct CopilotButton { fs: Arc, } -impl Entity for CopilotButton { - type Event = (); -} - -impl View for CopilotButton { - fn ui_name() -> &'static str { - "CopilotButton" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +impl Render for CopilotButton { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { let all_language_settings = all_language_settings(None, cx); if !all_language_settings.copilot.feature_enabled { - return Empty::new().into_any(); + return div(); } - let theme = theme::current(cx).clone(); - let active = self.popup_menu.read(cx).visible(); let Some(copilot) = Copilot::global(cx) else { - return Empty::new().into_any(); + return div(); }; let status = copilot.read(cx).status(); @@ -60,59 +49,26 @@ impl View for CopilotButton { .editor_enabled .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None)); - Stack::new() - .with_child( - MouseEventHandler::new::(0, cx, { - let theme = theme.clone(); - let status = status.clone(); - move |state, _cx| { - let style = theme - .workspace - .status_bar - .panel_buttons - .button - .in_state(active) - .style_for(state); + let icon = match status { + Status::Error(_) => Icon::CopilotError, + Status::Authorized => { + if enabled { + Icon::Copilot + } else { + Icon::CopilotDisabled + } + } + _ => Icon::CopilotInit, + }; - Flex::row() - .with_child( - Svg::new({ - match status { - Status::Error(_) => "icons/copilot_error.svg", - Status::Authorized => { - if enabled { - "icons/copilot.svg" - } else { - "icons/copilot_disabled.svg" - } - } - _ => "icons/copilot_init.svg", - } - }) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .aligned() - .into_any_named("copilot-icon"), - ) - .constrained() - .with_height(style.icon_size) - .contained() - .with_style(style.container) - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_down(MouseButton::Left, |_, this, cx| { - this.popup_menu.update(cx, |menu, _| menu.delay_cancel()); - }) - .on_click(MouseButton::Left, { - let status = status.clone(); - move |_, this, cx| match status { - Status::Authorized => this.deploy_copilot_menu(cx), - Status::Error(ref e) => { - if let Some(workspace) = cx.root_view().clone().downcast::() - { - workspace.update(cx, |workspace, cx| { + if let Status::Error(e) = status { + return div().child( + IconButton::new("copilot-error", icon) + .icon_size(IconSize::Small) + .on_click(cx.listener(move |_, _, cx| { + if let Some(workspace) = cx.window_handle().downcast::() { + workspace + .update(cx, |workspace, cx| { workspace.show_toast( Toast::new( COPILOT_ERROR_TOAST_ID, @@ -132,43 +88,40 @@ impl View for CopilotButton { ), cx, ); - }); - } + }) + .ok(); } - _ => this.deploy_copilot_start_menu(cx), + })) + .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)), + ); + } + let this = cx.view().clone(); + + div().child( + popover_menu("copilot") + .menu(move |cx| match status { + Status::Authorized => { + Some(this.update(cx, |this, cx| this.build_copilot_menu(cx))) } + _ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))), }) - .with_tooltip::( - 0, - "GitHub Copilot", - None, - theme.tooltip.clone(), - cx, + .anchor(AnchorCorner::BottomRight) + .trigger( + IconButton::new("copilot-icon", icon) + .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)), ), - ) - .with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right()) - .into_any() + ) } } impl CopilotButton { pub fn new(fs: Arc, cx: &mut ViewContext) -> Self { - let button_view_id = cx.view_id(); - let menu = cx.add_view(|cx| { - let mut menu = ContextMenu::new(button_view_id, cx); - menu.set_position_mode(OverlayPositionMode::Local); - menu - }); - - cx.observe(&menu, |_, _, cx| cx.notify()).detach(); - Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); - cx.observe_global::(move |_, cx| cx.notify()) + cx.observe_global::(move |_, cx| cx.notify()) .detach(); Self { - popup_menu: menu, editor_subscription: None, editor_enabled: None, language: None, @@ -177,108 +130,91 @@ impl CopilotButton { } } - pub fn deploy_copilot_start_menu(&mut self, cx: &mut ViewContext) { - let mut menu_options = Vec::with_capacity(2); + pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext) -> View { let fs = self.fs.clone(); - - menu_options.push(ContextMenuItem::handler("Sign In", |cx| { - initiate_sign_in(cx) - })); - menu_options.push(ContextMenuItem::handler("Disable Copilot", move |cx| { - hide_copilot(fs.clone(), cx) - })); - - self.popup_menu.update(cx, |menu, cx| { - menu.toggle( - Default::default(), - AnchorCorner::BottomRight, - menu_options, - cx, - ); - }); + ContextMenu::build(cx, |menu, _| { + menu.entry("Sign In", None, initiate_sign_in).entry( + "Disable Copilot", + None, + move |cx| hide_copilot(fs.clone(), cx), + ) + }) } - pub fn deploy_copilot_menu(&mut self, cx: &mut ViewContext) { + pub fn build_copilot_menu(&mut self, cx: &mut ViewContext) -> View { let fs = self.fs.clone(); - let mut menu_options = Vec::with_capacity(8); - if let Some(language) = self.language.clone() { - let fs = fs.clone(); - let language_enabled = language_settings::language_settings(Some(&language), None, cx) - .show_copilot_suggestions; - menu_options.push(ContextMenuItem::handler( - format!( - "{} Suggestions for {}", - if language_enabled { "Hide" } else { "Show" }, - language.name() - ), - move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), - )); - } + return ContextMenu::build(cx, move |mut menu, cx| { + if let Some(language) = self.language.clone() { + let fs = fs.clone(); + let language_enabled = + language_settings::language_settings(Some(&language), None, cx) + .show_copilot_suggestions; - let settings = settings::get::(cx); + menu = menu.entry( + format!( + "{} Suggestions for {}", + if language_enabled { "Hide" } else { "Show" }, + language.name() + ), + None, + move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), + ); + } - if let Some(file) = &self.file { - let path = file.path().clone(); - let path_enabled = settings.copilot_enabled_for_path(&path); - menu_options.push(ContextMenuItem::handler( - format!( - "{} Suggestions for This Path", - if path_enabled { "Hide" } else { "Show" } - ), - move |cx| { - if let Some(workspace) = cx.root_view().clone().downcast::() { - let workspace = workspace.downgrade(); - cx.spawn(|_, cx| { - configure_disabled_globs( - workspace, - path_enabled.then_some(path.clone()), - cx, - ) - }) - .detach_and_log_err(cx); - } + let settings = AllLanguageSettings::get_global(cx); + + if let Some(file) = &self.file { + let path = file.path().clone(); + let path_enabled = settings.copilot_enabled_for_path(&path); + + menu = menu.entry( + format!( + "{} Suggestions for This Path", + if path_enabled { "Hide" } else { "Show" } + ), + None, + move |cx| { + if let Some(workspace) = cx.window_handle().downcast::() { + if let Ok(workspace) = workspace.root_view(cx) { + let workspace = workspace.downgrade(); + cx.spawn(|cx| { + configure_disabled_globs( + workspace, + path_enabled.then_some(path.clone()), + cx, + ) + }) + .detach_and_log_err(cx); + } + } + }, + ); + } + + let globally_enabled = settings.copilot_enabled(None, None); + menu.entry( + if globally_enabled { + "Hide Suggestions for All Files" + } else { + "Show Suggestions for All Files" }, - )); - } - - let globally_enabled = settings.copilot_enabled(None, None); - menu_options.push(ContextMenuItem::handler( - if globally_enabled { - "Hide Suggestions for All Files" - } else { - "Show Suggestions for All Files" - }, - move |cx| toggle_copilot_globally(fs.clone(), cx), - )); - - menu_options.push(ContextMenuItem::Separator); - - let icon_style = theme::current(cx).copilot.out_link_icon.clone(); - menu_options.push(ContextMenuItem::action( - move |state: &mut MouseState, style: &theme::ContextMenuItem| { - Flex::row() - .with_child(Label::new("Copilot Settings", style.label.clone())) - .with_child(theme::ui::icon(icon_style.style_for(state))) - .align_children_center() - .into_any() - }, - OsOpen::new(COPILOT_SETTINGS_URL), - )); - - menu_options.push(ContextMenuItem::action("Sign Out", SignOut)); - - self.popup_menu.update(cx, |menu, cx| { - menu.toggle( - Default::default(), - AnchorCorner::BottomRight, - menu_options, - cx, - ); + None, + move |cx| toggle_copilot_globally(fs.clone(), cx), + ) + .separator() + .link( + "Copilot Settings", + OpenBrowser { + url: COPILOT_SETTINGS_URL.to_string(), + } + .boxed_clone(), + ) + .action("Sign Out", SignOut.boxed_clone()) }); } - pub fn update_enabled(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + pub fn update_enabled(&mut self, editor: View, cx: &mut ViewContext) { let editor = editor.read(cx); let snapshot = editor.buffer().read(cx).snapshot(cx); let suggestion_anchor = editor.selections.newest_anchor().start; @@ -299,8 +235,10 @@ impl CopilotButton { impl StatusItemView for CopilotButton { fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { if let Some(editor) = item.map(|item| item.act_as::(cx)).flatten() { - self.editor_subscription = - Some((cx.observe(&editor, Self::update_enabled), editor.id())); + self.editor_subscription = Some(( + cx.observe(&editor, Self::update_enabled), + editor.entity_id().as_u64() as usize, + )); self.update_enabled(editor, cx); } else { self.language = None; @@ -312,9 +250,9 @@ impl StatusItemView for CopilotButton { } async fn configure_disabled_globs( - workspace: WeakViewHandle, + workspace: WeakView, path_to_disable: Option>, - mut cx: AsyncAppContext, + mut cx: AsyncWindowContext, ) -> Result<()> { let settings_editor = workspace .update(&mut cx, |_, cx| { @@ -396,20 +334,23 @@ fn initiate_sign_in(cx: &mut WindowContext) { match status { Status::Starting { task } => { - let Some(workspace) = cx.root_view().clone().downcast::() else { + let Some(workspace) = cx.window_handle().downcast::() else { return; }; - workspace.update(cx, |workspace, cx| { + let Ok(workspace) = workspace.update(cx, |workspace, cx| { workspace.show_toast( Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."), cx, - ) - }); - let workspace = workspace.downgrade(); + ); + workspace.weak_handle() + }) else { + return; + }; + cx.spawn(|mut cx| async move { task.await; - if let Some(copilot) = cx.read(Copilot::global) { + if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() { workspace .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { Status::Authorized => workspace.show_toast( diff --git a/crates/copilot_button2/Cargo.toml b/crates/copilot_button2/Cargo.toml deleted file mode 100644 index 9793ecfb15..0000000000 --- a/crates/copilot_button2/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "copilot_button2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/copilot_button.rs" -doctest = false - -[dependencies] -copilot = { package = "copilot2", path = "../copilot2" } -editor = { package = "editor2", path = "../editor2" } -fs = { package = "fs2", path = "../fs2" } -zed-actions = { package="zed_actions2", path = "../zed_actions2"} -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -settings = { package = "settings2", path = "../settings2" } -theme = { package = "theme2", path = "../theme2" } -util = { path = "../util" } -workspace = { package = "workspace2", path = "../workspace2" } -anyhow.workspace = true -smol.workspace = true -futures.workspace = true - -[dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/copilot_button2/src/copilot_button.rs b/crates/copilot_button2/src/copilot_button.rs deleted file mode 100644 index 60b25fee12..0000000000 --- a/crates/copilot_button2/src/copilot_button.rs +++ /dev/null @@ -1,378 +0,0 @@ -use anyhow::Result; -use copilot::{Copilot, SignOut, Status}; -use editor::{scroll::autoscroll::Autoscroll, Editor}; -use fs::Fs; -use gpui::{ - div, Action, AnchorCorner, AppContext, AsyncWindowContext, Entity, IntoElement, ParentElement, - Render, Subscription, View, ViewContext, WeakView, WindowContext, -}; -use language::{ - language_settings::{self, all_language_settings, AllLanguageSettings}, - File, Language, -}; -use settings::{update_settings_file, Settings, SettingsStore}; -use std::{path::Path, sync::Arc}; -use util::{paths, ResultExt}; -use workspace::{ - create_and_open_local_file, - item::ItemHandle, - ui::{popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, IconSize, Tooltip}, - StatusItemView, Toast, Workspace, -}; -use zed_actions::OpenBrowser; - -const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot"; -const COPILOT_STARTING_TOAST_ID: usize = 1337; -const COPILOT_ERROR_TOAST_ID: usize = 1338; - -pub struct CopilotButton { - editor_subscription: Option<(Subscription, usize)>, - editor_enabled: Option, - language: Option>, - file: Option>, - fs: Arc, -} - -impl Render for CopilotButton { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let all_language_settings = all_language_settings(None, cx); - if !all_language_settings.copilot.feature_enabled { - return div(); - } - - let Some(copilot) = Copilot::global(cx) else { - return div(); - }; - let status = copilot.read(cx).status(); - - let enabled = self - .editor_enabled - .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None)); - - let icon = match status { - Status::Error(_) => Icon::CopilotError, - Status::Authorized => { - if enabled { - Icon::Copilot - } else { - Icon::CopilotDisabled - } - } - _ => Icon::CopilotInit, - }; - - if let Status::Error(e) = status { - return div().child( - IconButton::new("copilot-error", icon) - .icon_size(IconSize::Small) - .on_click(cx.listener(move |_, _, cx| { - if let Some(workspace) = cx.window_handle().downcast::() { - workspace - .update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new( - COPILOT_ERROR_TOAST_ID, - format!("Copilot can't be started: {}", e), - ) - .on_click( - "Reinstall Copilot", - |cx| { - if let Some(copilot) = Copilot::global(cx) { - copilot - .update(cx, |copilot, cx| { - copilot.reinstall(cx) - }) - .detach(); - } - }, - ), - cx, - ); - }) - .ok(); - } - })) - .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)), - ); - } - let this = cx.view().clone(); - - div().child( - popover_menu("copilot") - .menu(move |cx| match status { - Status::Authorized => { - Some(this.update(cx, |this, cx| this.build_copilot_menu(cx))) - } - _ => Some(this.update(cx, |this, cx| this.build_copilot_start_menu(cx))), - }) - .anchor(AnchorCorner::BottomRight) - .trigger( - IconButton::new("copilot-icon", icon) - .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)), - ), - ) - } -} - -impl CopilotButton { - pub fn new(fs: Arc, cx: &mut ViewContext) -> Self { - Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach()); - - cx.observe_global::(move |_, cx| cx.notify()) - .detach(); - - Self { - editor_subscription: None, - editor_enabled: None, - language: None, - file: None, - fs, - } - } - - pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext) -> View { - let fs = self.fs.clone(); - ContextMenu::build(cx, |menu, _| { - menu.entry("Sign In", None, initiate_sign_in).entry( - "Disable Copilot", - None, - move |cx| hide_copilot(fs.clone(), cx), - ) - }) - } - - pub fn build_copilot_menu(&mut self, cx: &mut ViewContext) -> View { - let fs = self.fs.clone(); - - return ContextMenu::build(cx, move |mut menu, cx| { - if let Some(language) = self.language.clone() { - let fs = fs.clone(); - let language_enabled = - language_settings::language_settings(Some(&language), None, cx) - .show_copilot_suggestions; - - menu = menu.entry( - format!( - "{} Suggestions for {}", - if language_enabled { "Hide" } else { "Show" }, - language.name() - ), - None, - move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx), - ); - } - - let settings = AllLanguageSettings::get_global(cx); - - if let Some(file) = &self.file { - let path = file.path().clone(); - let path_enabled = settings.copilot_enabled_for_path(&path); - - menu = menu.entry( - format!( - "{} Suggestions for This Path", - if path_enabled { "Hide" } else { "Show" } - ), - None, - move |cx| { - if let Some(workspace) = cx.window_handle().downcast::() { - if let Ok(workspace) = workspace.root_view(cx) { - let workspace = workspace.downgrade(); - cx.spawn(|cx| { - configure_disabled_globs( - workspace, - path_enabled.then_some(path.clone()), - cx, - ) - }) - .detach_and_log_err(cx); - } - } - }, - ); - } - - let globally_enabled = settings.copilot_enabled(None, None); - menu.entry( - if globally_enabled { - "Hide Suggestions for All Files" - } else { - "Show Suggestions for All Files" - }, - None, - move |cx| toggle_copilot_globally(fs.clone(), cx), - ) - .separator() - .link( - "Copilot Settings", - OpenBrowser { - url: COPILOT_SETTINGS_URL.to_string(), - } - .boxed_clone(), - ) - .action("Sign Out", SignOut.boxed_clone()) - }); - } - - pub fn update_enabled(&mut self, editor: View, cx: &mut ViewContext) { - let editor = editor.read(cx); - let snapshot = editor.buffer().read(cx).snapshot(cx); - let suggestion_anchor = editor.selections.newest_anchor().start; - let language = snapshot.language_at(suggestion_anchor); - let file = snapshot.file_at(suggestion_anchor).cloned(); - - self.editor_enabled = Some( - all_language_settings(self.file.as_ref(), cx) - .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())), - ); - self.language = language.cloned(); - self.file = file; - - cx.notify() - } -} - -impl StatusItemView for CopilotButton { - fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext) { - if let Some(editor) = item.map(|item| item.act_as::(cx)).flatten() { - self.editor_subscription = Some(( - cx.observe(&editor, Self::update_enabled), - editor.entity_id().as_u64() as usize, - )); - self.update_enabled(editor, cx); - } else { - self.language = None; - self.editor_subscription = None; - self.editor_enabled = None; - } - cx.notify(); - } -} - -async fn configure_disabled_globs( - workspace: WeakView, - path_to_disable: Option>, - mut cx: AsyncWindowContext, -) -> Result<()> { - let settings_editor = workspace - .update(&mut cx, |_, cx| { - create_and_open_local_file(&paths::SETTINGS, cx, || { - settings::initial_user_settings_content().as_ref().into() - }) - })? - .await? - .downcast::() - .unwrap(); - - settings_editor.downgrade().update(&mut cx, |item, cx| { - let text = item.buffer().read(cx).snapshot(cx).text(); - - let settings = cx.global::(); - let edits = settings.edits_for_update::(&text, |file| { - let copilot = file.copilot.get_or_insert_with(Default::default); - let globs = copilot.disabled_globs.get_or_insert_with(|| { - settings - .get::(None) - .copilot - .disabled_globs - .iter() - .map(|glob| glob.glob().to_string()) - .collect() - }); - - if let Some(path_to_disable) = &path_to_disable { - globs.push(path_to_disable.to_string_lossy().into_owned()); - } else { - globs.clear(); - } - }); - - if !edits.is_empty() { - item.change_selections(Some(Autoscroll::newest()), cx, |selections| { - selections.select_ranges(edits.iter().map(|e| e.0.clone())); - }); - - // When *enabling* a path, don't actually perform an edit, just select the range. - if path_to_disable.is_some() { - item.edit(edits.iter().cloned(), cx); - } - } - })?; - - anyhow::Ok(()) -} - -fn toggle_copilot_globally(fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None); - update_settings_file::(fs, cx, move |file| { - file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into()) - }); -} - -fn toggle_copilot_for_language(language: Arc, fs: Arc, cx: &mut AppContext) { - let show_copilot_suggestions = - all_language_settings(None, cx).copilot_enabled(Some(&language), None); - update_settings_file::(fs, cx, move |file| { - file.languages - .entry(language.name()) - .or_default() - .show_copilot_suggestions = Some(!show_copilot_suggestions); - }); -} - -fn hide_copilot(fs: Arc, cx: &mut AppContext) { - update_settings_file::(fs, cx, move |file| { - file.features.get_or_insert(Default::default()).copilot = Some(false); - }); -} - -fn initiate_sign_in(cx: &mut WindowContext) { - let Some(copilot) = Copilot::global(cx) else { - return; - }; - let status = copilot.read(cx).status(); - - match status { - Status::Starting { task } => { - let Some(workspace) = cx.window_handle().downcast::() else { - return; - }; - - let Ok(workspace) = workspace.update(cx, |workspace, cx| { - workspace.show_toast( - Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."), - cx, - ); - workspace.weak_handle() - }) else { - return; - }; - - cx.spawn(|mut cx| async move { - task.await; - if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() { - workspace - .update(&mut cx, |workspace, cx| match copilot.read(cx).status() { - Status::Authorized => workspace.show_toast( - Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"), - cx, - ), - _ => { - workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx); - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - } - }) - .log_err(); - } - }) - .detach(); - } - _ => { - copilot - .update(cx, |copilot, cx| copilot.sign_in(cx)) - .detach_and_log_err(cx); - } - } -} diff --git a/crates/language_selector/Cargo.toml b/crates/language_selector/Cargo.toml index f6e213f25f..6c03686200 100644 --- a/crates/language_selector/Cargo.toml +++ b/crates/language_selector/Cargo.toml @@ -9,17 +9,18 @@ path = "src/language_selector.rs" doctest = false [dependencies] -editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -language = { path = "../language" } -gpui = { path = "../gpui" } -picker = { path = "../picker" } -project = { path = "../project" } -theme = { path = "../theme" } -settings = { path = "../settings" } +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +language = { package = "language2", path = "../language2" } +gpui = { package = "gpui2", path = "../gpui2" } +picker = { package = "picker2", path = "../picker2" } +project = { package = "project2", path = "../project2" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } -workspace = { path = "../workspace" } +workspace = { package = "workspace2", path = "../workspace2" } anyhow.workspace = true [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/language_selector/src/active_buffer_language.rs b/crates/language_selector/src/active_buffer_language.rs index 01333c1ffb..d5f177f7d6 100644 --- a/crates/language_selector/src/active_buffer_language.rs +++ b/crates/language_selector/src/active_buffer_language.rs @@ -1,15 +1,14 @@ use editor::Editor; -use gpui::{ - elements::*, - platform::{CursorStyle, MouseButton}, - Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, -}; +use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView}; use std::sync::Arc; +use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip}; use workspace::{item::ItemHandle, StatusItemView, Workspace}; +use crate::LanguageSelector; + pub struct ActiveBufferLanguage { active_language: Option>>, - workspace: WeakViewHandle, + workspace: WeakView, _observe_active_editor: Option, } @@ -22,7 +21,7 @@ impl ActiveBufferLanguage { } } - fn update_language(&mut self, editor: ViewHandle, cx: &mut ViewContext) { + fn update_language(&mut self, editor: View, cx: &mut ViewContext) { self.active_language = Some(None); let editor = editor.read(cx); @@ -36,44 +35,28 @@ impl ActiveBufferLanguage { } } -impl Entity for ActiveBufferLanguage { - type Event = (); -} - -impl View for ActiveBufferLanguage { - fn ui_name() -> &'static str { - "ActiveBufferLanguage" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(active_language) = self.active_language.as_ref() { +impl Render for ActiveBufferLanguage { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + div().when_some(self.active_language.as_ref(), |el, active_language| { let active_language_text = if let Some(active_language_text) = active_language { active_language_text.to_string() } else { "Unknown".to_string() }; - let theme = theme::current(cx).clone(); - MouseEventHandler::new::(0, cx, |state, cx| { - let theme = &theme::current(cx).workspace.status_bar; - let style = theme.active_language.style_for(state); - Label::new(active_language_text, style.text.clone()) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - crate::toggle(workspace, &Default::default(), cx) - }); - } - }) - .with_tooltip::(0, "Select Language", None, theme.tooltip.clone(), cx) - .into_any() - } else { - Empty::new().into_any() - } + el.child( + Button::new("change-language", active_language_text) + .label_size(LabelSize::Small) + .on_click(cx.listener(|this, _, cx| { + if let Some(workspace) = this.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + LanguageSelector::toggle(workspace, cx) + }); + } + })) + .tooltip(|cx| Tooltip::text("Select Language", cx)), + ) + }) } } diff --git a/crates/language_selector/src/language_selector.rs b/crates/language_selector/src/language_selector.rs index b5336d5b3b..33b2e48126 100644 --- a/crates/language_selector/src/language_selector.rs +++ b/crates/language_selector/src/language_selector.rs @@ -4,46 +4,87 @@ pub use active_buffer_language::ActiveBufferLanguage; use anyhow::anyhow; use editor::Editor; use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; -use gpui::{actions, elements::*, AppContext, ModelHandle, MouseState, ViewContext}; +use gpui::{ + actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, + ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, +}; use language::{Buffer, LanguageRegistry}; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{Picker, PickerDelegate}; use project::Project; use std::sync::Arc; +use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; -use workspace::Workspace; +use workspace::{ModalView, Workspace}; actions!(language_selector, [Toggle]); pub fn init(cx: &mut AppContext) { - Picker::::init(cx); - cx.add_action(toggle); + cx.observe_new_views(LanguageSelector::register).detach(); } -pub fn toggle( - workspace: &mut Workspace, - _: &Toggle, - cx: &mut ViewContext, -) -> Option<()> { - let (_, buffer, _) = workspace - .active_item(cx)? - .act_as::(cx)? - .read(cx) - .active_excerpt(cx)?; - workspace.toggle_modal(cx, |workspace, cx| { - let registry = workspace.app_state().languages.clone(); - cx.add_view(|cx| { - Picker::new( - LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry), - cx, - ) - }) - }); - Some(()) +pub struct LanguageSelector { + picker: View>, } +impl LanguageSelector { + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(move |workspace, _: &Toggle, cx| { + Self::toggle(workspace, cx); + }); + } + + fn toggle(workspace: &mut Workspace, cx: &mut ViewContext) -> Option<()> { + let registry = workspace.app_state().languages.clone(); + let (_, buffer, _) = workspace + .active_item(cx)? + .act_as::(cx)? + .read(cx) + .active_excerpt(cx)?; + let project = workspace.project().clone(); + + workspace.toggle_modal(cx, move |cx| { + LanguageSelector::new(buffer, project, registry, cx) + }); + Some(()) + } + + fn new( + buffer: Model, + project: Model, + language_registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let delegate = LanguageSelectorDelegate::new( + cx.view().downgrade(), + buffer, + project, + language_registry, + ); + + let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +impl Render for LanguageSelector { + fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { + v_stack().w(rems(34.)).child(self.picker.clone()) + } +} + +impl FocusableView for LanguageSelector { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl EventEmitter for LanguageSelector {} +impl ModalView for LanguageSelector {} + pub struct LanguageSelectorDelegate { - buffer: ModelHandle, - project: ModelHandle, + language_selector: WeakView, + buffer: Model, + project: Model, language_registry: Arc, candidates: Vec, matches: Vec, @@ -52,8 +93,9 @@ pub struct LanguageSelectorDelegate { impl LanguageSelectorDelegate { fn new( - buffer: ModelHandle, - project: ModelHandle, + language_selector: WeakView, + buffer: Model, + project: Model, language_registry: Arc, ) -> Self { let candidates = language_registry @@ -62,29 +104,22 @@ impl LanguageSelectorDelegate { .enumerate() .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) .collect::>(); - let mut matches = candidates - .iter() - .map(|candidate| StringMatch { - candidate_id: candidate.id, - score: 0., - positions: Default::default(), - string: candidate.string.clone(), - }) - .collect::>(); - matches.sort_unstable_by(|mat1, mat2| mat1.string.cmp(&mat2.string)); Self { + language_selector, buffer, project, language_registry, candidates, - matches, + matches: vec![], selected_index: 0, } } } impl PickerDelegate for LanguageSelectorDelegate { + type ListItem = ListItem; + fn placeholder_text(&self) -> Arc { "Select a language...".into() } @@ -102,23 +137,25 @@ impl PickerDelegate for LanguageSelectorDelegate { cx.spawn(|_, mut cx| async move { let language = language.await?; let project = project - .upgrade(&cx) + .upgrade() .ok_or_else(|| anyhow!("project was dropped"))?; let buffer = buffer - .upgrade(&cx) + .upgrade() .ok_or_else(|| anyhow!("buffer was dropped"))?; project.update(&mut cx, |project, cx| { project.set_language_for_buffer(&buffer, language, cx); - }); - anyhow::Ok(()) + }) }) .detach_and_log_err(cx); } - - cx.emit(PickerEvent::Dismiss); + self.dismissed(cx); } - fn dismissed(&mut self, _cx: &mut ViewContext>) {} + fn dismissed(&mut self, cx: &mut ViewContext>) { + self.language_selector + .update(cx, |_, cx| cx.emit(DismissEvent)) + .log_err(); + } fn selected_index(&self) -> usize { self.selected_index @@ -133,7 +170,7 @@ impl PickerDelegate for LanguageSelectorDelegate { query: String, cx: &mut ViewContext>, ) -> gpui::Task<()> { - let background = cx.background().clone(); + let background = cx.background_executor().clone(); let candidates = self.candidates.clone(); cx.spawn(|this, mut cx| async move { let matches = if query.is_empty() { @@ -160,7 +197,7 @@ impl PickerDelegate for LanguageSelectorDelegate { }; this.update(&mut cx, |this, cx| { - let delegate = this.delegate_mut(); + let delegate = &mut this.delegate; delegate.matches = matches; delegate.selected_index = delegate .selected_index @@ -174,23 +211,22 @@ impl PickerDelegate for LanguageSelectorDelegate { fn render_match( &self, ix: usize, - mouse_state: &mut MouseState, selected: bool, - cx: &AppContext, - ) -> AnyElement> { - let theme = theme::current(cx); + cx: &mut ViewContext>, + ) -> Option { let mat = &self.matches[ix]; - let style = theme.picker.item.in_state(selected).style_for(mouse_state); let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); let mut label = mat.string.clone(); if buffer_language_name.as_deref() == Some(mat.string.as_str()) { label.push_str(" (current)"); } - Label::new(label, style.label.clone()) - .with_highlights(mat.positions.clone()) - .contained() - .with_style(style.container) - .into_any() + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child(HighlightedLabel::new(label, mat.positions.clone())), + ) } } diff --git a/crates/language_selector2/Cargo.toml b/crates/language_selector2/Cargo.toml deleted file mode 100644 index 67f0d1e0ee..0000000000 --- a/crates/language_selector2/Cargo.toml +++ /dev/null @@ -1,26 +0,0 @@ -[package] -name = "language_selector2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/language_selector.rs" -doctest = false - -[dependencies] -editor = { package = "editor2", path = "../editor2" } -fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -language = { package = "language2", path = "../language2" } -gpui = { package = "gpui2", path = "../gpui2" } -picker = { package = "picker2", path = "../picker2" } -project = { package = "project2", path = "../project2" } -theme = { package = "theme2", path = "../theme2" } -ui = { package = "ui2", path = "../ui2" } -settings = { package = "settings2", path = "../settings2" } -util = { path = "../util" } -workspace = { package = "workspace2", path = "../workspace2" } -anyhow.workspace = true - -[dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/language_selector2/src/active_buffer_language.rs b/crates/language_selector2/src/active_buffer_language.rs deleted file mode 100644 index d5f177f7d6..0000000000 --- a/crates/language_selector2/src/active_buffer_language.rs +++ /dev/null @@ -1,79 +0,0 @@ -use editor::Editor; -use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView}; -use std::sync::Arc; -use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip}; -use workspace::{item::ItemHandle, StatusItemView, Workspace}; - -use crate::LanguageSelector; - -pub struct ActiveBufferLanguage { - active_language: Option>>, - workspace: WeakView, - _observe_active_editor: Option, -} - -impl ActiveBufferLanguage { - pub fn new(workspace: &Workspace) -> Self { - Self { - active_language: None, - workspace: workspace.weak_handle(), - _observe_active_editor: None, - } - } - - fn update_language(&mut self, editor: View, cx: &mut ViewContext) { - self.active_language = Some(None); - - let editor = editor.read(cx); - if let Some((_, buffer, _)) = editor.active_excerpt(cx) { - if let Some(language) = buffer.read(cx).language() { - self.active_language = Some(Some(language.name())); - } - } - - cx.notify(); - } -} - -impl Render for ActiveBufferLanguage { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - div().when_some(self.active_language.as_ref(), |el, active_language| { - let active_language_text = if let Some(active_language_text) = active_language { - active_language_text.to_string() - } else { - "Unknown".to_string() - }; - - el.child( - Button::new("change-language", active_language_text) - .label_size(LabelSize::Small) - .on_click(cx.listener(|this, _, cx| { - if let Some(workspace) = this.workspace.upgrade() { - workspace.update(cx, |workspace, cx| { - LanguageSelector::toggle(workspace, cx) - }); - } - })) - .tooltip(|cx| Tooltip::text("Select Language", cx)), - ) - }) - } -} - -impl StatusItemView for ActiveBufferLanguage { - fn set_active_pane_item( - &mut self, - active_pane_item: Option<&dyn ItemHandle>, - cx: &mut ViewContext, - ) { - if let Some(editor) = active_pane_item.and_then(|item| item.act_as::(cx)) { - self._observe_active_editor = Some(cx.observe(&editor, Self::update_language)); - self.update_language(editor, cx); - } else { - self.active_language = None; - self._observe_active_editor = None; - } - - cx.notify(); - } -} diff --git a/crates/language_selector2/src/language_selector.rs b/crates/language_selector2/src/language_selector.rs deleted file mode 100644 index 33b2e48126..0000000000 --- a/crates/language_selector2/src/language_selector.rs +++ /dev/null @@ -1,232 +0,0 @@ -mod active_buffer_language; - -pub use active_buffer_language::ActiveBufferLanguage; -use anyhow::anyhow; -use editor::Editor; -use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; -use gpui::{ - actions, AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model, - ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, -}; -use language::{Buffer, LanguageRegistry}; -use picker::{Picker, PickerDelegate}; -use project::Project; -use std::sync::Arc; -use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; -use util::ResultExt; -use workspace::{ModalView, Workspace}; - -actions!(language_selector, [Toggle]); - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(LanguageSelector::register).detach(); -} - -pub struct LanguageSelector { - picker: View>, -} - -impl LanguageSelector { - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(move |workspace, _: &Toggle, cx| { - Self::toggle(workspace, cx); - }); - } - - fn toggle(workspace: &mut Workspace, cx: &mut ViewContext) -> Option<()> { - let registry = workspace.app_state().languages.clone(); - let (_, buffer, _) = workspace - .active_item(cx)? - .act_as::(cx)? - .read(cx) - .active_excerpt(cx)?; - let project = workspace.project().clone(); - - workspace.toggle_modal(cx, move |cx| { - LanguageSelector::new(buffer, project, registry, cx) - }); - Some(()) - } - - fn new( - buffer: Model, - project: Model, - language_registry: Arc, - cx: &mut ViewContext, - ) -> Self { - let delegate = LanguageSelectorDelegate::new( - cx.view().downgrade(), - buffer, - project, - language_registry, - ); - - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); - Self { picker } - } -} - -impl Render for LanguageSelector { - fn render(&mut self, _cx: &mut ViewContext) -> impl IntoElement { - v_stack().w(rems(34.)).child(self.picker.clone()) - } -} - -impl FocusableView for LanguageSelector { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl EventEmitter for LanguageSelector {} -impl ModalView for LanguageSelector {} - -pub struct LanguageSelectorDelegate { - language_selector: WeakView, - buffer: Model, - project: Model, - language_registry: Arc, - candidates: Vec, - matches: Vec, - selected_index: usize, -} - -impl LanguageSelectorDelegate { - fn new( - language_selector: WeakView, - buffer: Model, - project: Model, - language_registry: Arc, - ) -> Self { - let candidates = language_registry - .language_names() - .into_iter() - .enumerate() - .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name)) - .collect::>(); - - Self { - language_selector, - buffer, - project, - language_registry, - candidates, - matches: vec![], - selected_index: 0, - } - } -} - -impl PickerDelegate for LanguageSelectorDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self) -> Arc { - "Select a language...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if let Some(mat) = self.matches.get(self.selected_index) { - let language_name = &self.candidates[mat.candidate_id].string; - let language = self.language_registry.language_for_name(language_name); - let project = self.project.downgrade(); - let buffer = self.buffer.downgrade(); - cx.spawn(|_, mut cx| async move { - let language = language.await?; - let project = project - .upgrade() - .ok_or_else(|| anyhow!("project was dropped"))?; - let buffer = buffer - .upgrade() - .ok_or_else(|| anyhow!("buffer was dropped"))?; - project.update(&mut cx, |project, cx| { - project.set_language_for_buffer(&buffer, language, cx); - }) - }) - .detach_and_log_err(cx); - } - self.dismissed(cx); - } - - fn dismissed(&mut self, cx: &mut ViewContext>) { - self.language_selector - .update(cx, |_, cx| cx.emit(DismissEvent)) - .log_err(); - } - - fn selected_index(&self) -> usize { - self.selected_index - } - - fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext>) { - self.selected_index = ix; - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> gpui::Task<()> { - let background = cx.background_executor().clone(); - let candidates = self.candidates.clone(); - cx.spawn(|this, mut cx| async move { - let matches = if query.is_empty() { - candidates - .into_iter() - .enumerate() - .map(|(index, candidate)| StringMatch { - candidate_id: index, - string: candidate.string, - positions: Vec::new(), - score: 0.0, - }) - .collect() - } else { - match_strings( - &candidates, - &query, - false, - 100, - &Default::default(), - background, - ) - .await - }; - - this.update(&mut cx, |this, cx| { - let delegate = &mut this.delegate; - delegate.matches = matches; - delegate.selected_index = delegate - .selected_index - .min(delegate.matches.len().saturating_sub(1)); - cx.notify(); - }) - .log_err(); - }) - } - - fn render_match( - &self, - ix: usize, - selected: bool, - cx: &mut ViewContext>, - ) -> Option { - let mat = &self.matches[ix]; - let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name()); - let mut label = mat.string.clone(); - if buffer_language_name.as_deref() == Some(mat.string.as_str()) { - label.push_str(" (current)"); - } - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child(HighlightedLabel::new(label, mat.positions.clone())), - ) - } -} diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 4fe5372a51..56352f254f 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -9,32 +9,33 @@ path = "src/project_panel.rs" doctest = false [dependencies] -context_menu = { path = "../context_menu" } collections = { path = "../collections" } -db = { path = "../db" } -drag_and_drop = { path = "../drag_and_drop" } -editor = { path = "../editor" } -gpui = { path = "../gpui" } -menu = { path = "../menu" } -project = { path = "../project" } -settings = { path = "../settings" } -theme = { path = "../theme" } +db = { path = "../db2", package = "db2" } +editor = { path = "../editor2", package = "editor2" } +gpui = { path = "../gpui2", package = "gpui2" } +menu = { path = "../menu2", package = "menu2" } +project = { path = "../project2", package = "project2" } +search = { package = "search2", path = "../search2" } +settings = { path = "../settings2", package = "settings2" } +theme = { path = "../theme2", package = "theme2" } +ui = { path = "../ui2", package = "ui2" } util = { path = "../util" } -workspace = { path = "../workspace" } +workspace = { path = "../workspace2", package = "workspace2" } +anyhow.workspace = true postage.workspace = true futures.workspace = true serde.workspace = true serde_derive.workspace = true serde_json.workspace = true -anyhow.workspace = true schemars.workspace = true +smallvec.workspace = true pretty_assertions.workspace = true unicase = "2.6" [dev-dependencies] -client = { path = "../client", features = ["test-support"] } -language = { path = "../language", features = ["test-support"] } -editor = { path = "../editor", features = ["test-support"] } -gpui = { path = "../gpui", features = ["test-support"] } -workspace = { path = "../workspace", features = ["test-support"] } +client = { path = "../client2", package = "client2", features = ["test-support"] } +language = { path = "../language2", package = "language2", features = ["test-support"] } +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } +gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } +workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } serde_json.workspace = true diff --git a/crates/project_panel/src/file_associations.rs b/crates/project_panel/src/file_associations.rs index 9e9a865f3e..82aebe7913 100644 --- a/crates/project_panel/src/file_associations.rs +++ b/crates/project_panel/src/file_associations.rs @@ -41,56 +41,47 @@ impl FileAssociations { }) } - pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { + pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; + + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + let suffix = path.icon_suffix()?; - // FIXME: Associate a type with the languages and have the file's langauge - // override these associations - maybe!({ - let suffix = path.icon_suffix()?; - - this.suffixes - .get(suffix) - .and_then(|type_str| this.types.get(type_str)) - .map(|type_config| type_config.icon.clone()) - }) - .or_else(|| this.types.get("default").map(|config| config.icon.clone())) - }) - .unwrap_or_else(|| Arc::from("".to_string())) - } - - pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; - - let key = if expanded { - EXPANDED_DIRECTORY_TYPE - } else { - COLLAPSED_DIRECTORY_TYPE - }; - - this.types - .get(key) + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) .map(|type_config| type_config.icon.clone()) }) - .unwrap_or_else(|| Arc::from("".to_string())) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) } - pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - let key = if expanded { - EXPANDED_CHEVRON_TYPE - } else { - COLLAPSED_CHEVRON_TYPE - }; + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + COLLAPSED_DIRECTORY_TYPE + }; - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - }) - .unwrap_or_else(|| Arc::from("".to_string())) + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + } + + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) } } diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 30d750cf46..8ab364e0c1 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,25 +1,18 @@ pub mod file_associations; mod project_panel_settings; +use settings::{Settings, SettingsStore}; -use context_menu::{ContextMenu, ContextMenuItem}; use db::kvp::KEY_VALUE_STORE; -use drag_and_drop::{DragAndDrop, Draggable}; use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; use file_associations::FileAssociations; -use futures::stream::StreamExt; +use anyhow::{anyhow, Result}; use gpui::{ - actions, - anyhow::{self, anyhow, Result}, - elements::{ - AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler, - ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, - }, - geometry::vector::Vector2F, - keymap_matcher::KeymapContext, - platform::{CursorStyle, MouseButton, PromptLevel}, - Action, AnyElement, AppContext, AssetSource, AsyncAppContext, ClipboardItem, Element, Entity, - ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, + actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, + KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, + Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, + VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -28,7 +21,6 @@ use project::{ }; use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; use serde::{Deserialize, Serialize}; -use settings::SettingsStore; use std::{ cmp::Ordering, collections::{hash_map, HashMap}, @@ -37,11 +29,12 @@ use std::{ path::Path, sync::Arc, }; -use theme::ProjectPanelEntry; +use theme::ThemeSettings; +use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem}; use unicase::UniCase; -use util::{ResultExt, TryFutureExt}; +use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ - dock::{DockPosition, Panel}, + dock::{DockPosition, Panel, PanelEvent}, Workspace, }; @@ -49,21 +42,21 @@ const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; pub struct ProjectPanel { - project: ModelHandle, + project: Model, fs: Arc, - list: UniformListState, + list: UniformListScrollHandle, + focus_handle: FocusHandle, visible_entries: Vec<(WorktreeId, Vec)>, last_worktree_root_id: Option, expanded_dir_ids: HashMap>, selection: Option, + context_menu: Option<(View, Point, Subscription)>, edit_state: Option, - filename_editor: ViewHandle, + filename_editor: View, clipboard_entry: Option, - context_menu: ViewHandle, - dragged_entry_destination: Option>, - workspace: WeakViewHandle, - has_focus: bool, - width: Option, + _dragged_entry_destination: Option>, + workspace: WeakView, + width: Option, pending_serialization: Task>, } @@ -94,7 +87,7 @@ pub enum ClipboardEntry { }, } -#[derive(Debug, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq, Clone)] pub struct EntryDetails { filename: String, icon: Option>, @@ -134,36 +127,19 @@ actions!( ); pub fn init_settings(cx: &mut AppContext) { - settings::register::(cx); + ProjectPanelSettings::register(cx); } pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); - cx.add_action(ProjectPanel::expand_selected_entry); - cx.add_action(ProjectPanel::collapse_selected_entry); - cx.add_action(ProjectPanel::collapse_all_entries); - cx.add_action(ProjectPanel::select_prev); - cx.add_action(ProjectPanel::select_next); - cx.add_action(ProjectPanel::new_file); - cx.add_action(ProjectPanel::new_directory); - cx.add_action(ProjectPanel::rename); - cx.add_async_action(ProjectPanel::delete); - cx.add_async_action(ProjectPanel::confirm); - cx.add_async_action(ProjectPanel::open_file); - cx.add_action(ProjectPanel::cancel); - cx.add_action(ProjectPanel::cut); - cx.add_action(ProjectPanel::copy); - cx.add_action(ProjectPanel::copy_path); - cx.add_action(ProjectPanel::copy_relative_path); - cx.add_action(ProjectPanel::reveal_in_finder); - cx.add_action(ProjectPanel::open_in_terminal); - cx.add_action(ProjectPanel::new_search_in_directory); - cx.add_action( - |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { - this.paste(action, cx); - }, - ); + + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }) + .detach(); } #[derive(Debug)] @@ -175,40 +151,45 @@ pub enum Event { SplitEntry { entry_id: ProjectEntryId, }, - DockPositionChanged, Focus, - NewSearchInDirectory { - dir_entry: Entry, - }, - ActivatePanel, } #[derive(Serialize, Deserialize)] struct SerializedProjectPanel { - width: Option, + width: Option, +} + +struct DraggedProjectEntryView { + entry_id: ProjectEntryId, + details: EntryDetails, + width: Pixels, } impl ProjectPanel { - fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { let project = workspace.project().clone(); - let project_panel = cx.add_view(|cx: &mut ViewContext| { + let project_panel = cx.new_view(|cx: &mut ViewContext| { cx.observe(&project, |this, _, cx| { this.update_visible_entries(None, cx); cx.notify(); }) .detach(); + let focus_handle = cx.focus_handle(); + + cx.on_focus(&focus_handle, Self::focus_in).detach(); + cx.subscribe(&project, |this, project, event, cx| match event { project::Event::ActiveEntryChanged(Some(entry_id)) => { - if settings::get::(cx).auto_reveal_entries { + if ProjectPanelSettings::get_global(cx).auto_reveal_entries { this.reveal_entry(project, *entry_id, true, cx); } } project::Event::RevealInProjectPanel(entry_id) => { this.reveal_entry(project, *entry_id, false, cx); - cx.emit(Event::ActivatePanel); + cx.emit(PanelEvent::Activate); } project::Event::ActivateProjectPanel => { - cx.emit(Event::ActivatePanel); + cx.emit(PanelEvent::Activate); } project::Event::WorktreeRemoved(id) => { this.expanded_dir_ids.remove(id); @@ -219,58 +200,47 @@ impl ProjectPanel { }) .detach(); - let filename_editor = cx.add_view(|cx| { - Editor::single_line( - Some(Arc::new(|theme| { - let mut style = theme.project_panel.filename_editor.clone(); - style.container.background_color.take(); - style - })), - cx, - ) - }); + let filename_editor = cx.new_view(|cx| Editor::single_line(cx)); cx.subscribe(&filename_editor, |this, _, event, cx| match event { - editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { + editor::EditorEvent::BufferEdited + | editor::EditorEvent::SelectionsChanged { .. } => { this.autoscroll(cx); } + editor::EditorEvent::Blurred => { + if this + .edit_state + .as_ref() + .map_or(false, |state| state.processing_filename.is_none()) + { + this.edit_state = None; + this.update_visible_entries(None, cx); + } + } _ => {} }) .detach(); - cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { - if !is_focused - && this - .edit_state - .as_ref() - .map_or(false, |state| state.processing_filename.is_none()) - { - this.edit_state = None; - this.update_visible_entries(None, cx); - } - }) - .detach(); - cx.observe_global::(|_, cx| { - cx.notify(); - }) - .detach(); + // cx.observe_global::(|_, cx| { + // cx.notify(); + // }) + // .detach(); - let view_id = cx.view_id(); let mut this = Self { project: project.clone(), fs: workspace.app_state().fs.clone(), - list: Default::default(), + list: UniformListScrollHandle::new(), + focus_handle, visible_entries: Default::default(), last_worktree_root_id: Default::default(), expanded_dir_ids: Default::default(), selection: None, edit_state: None, + context_menu: None, filename_editor, clipboard_entry: None, - context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - dragged_entry_destination: None, + _dragged_entry_destination: None, workspace: workspace.weak_handle(), - has_focus: false, width: None, pending_serialization: Task::ready(None), }; @@ -278,11 +248,12 @@ impl ProjectPanel { // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this, cx| { + ProjectPanelSettings::register(cx); + cx.observe_global::(move |this, cx| { let new_dock_position = this.position(cx); if new_dock_position != old_dock_position { old_dock_position = new_dock_position; - cx.emit(Event::DockPositionChanged); + cx.emit(PanelEvent::ChangePosition); } }) .detach(); @@ -311,8 +282,9 @@ impl ProjectPanel { ) .detach_and_log_err(cx); if !focus_opened_item { - if let Some(project_panel) = project_panel.upgrade(cx) { - cx.focus(&project_panel); + if let Some(project_panel) = project_panel.upgrade() { + let focus_handle = project_panel.read(cx).focus_handle.clone(); + cx.focus(&focus_handle); } } } @@ -320,16 +292,16 @@ impl ProjectPanel { } &Event::SplitEntry { entry_id } => { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { - workspace - .split_path( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: entry.path.clone(), - }, - cx, - ) - .detach_and_log_err(cx); + if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) { + // workspace + // .split_path( + // ProjectPath { + // worktree_id: worktree.read(cx).id(), + // path: entry.path.clone(), + // }, + // cx, + // ) + // .detach_and_log_err(cx); } } } @@ -341,38 +313,37 @@ impl ProjectPanel { project_panel } - pub fn load( - workspace: WeakViewHandle, - cx: AsyncAppContext, - ) -> Task>> { - cx.spawn(|mut cx| async move { - let serialized_panel = if let Some(panel) = cx - .background() - .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) - .await - .log_err() - .flatten() - { - Some(serde_json::from_str::(&panel)?) - } else { - None - }; - workspace.update(&mut cx, |workspace, cx| { - let panel = ProjectPanel::new(workspace, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; - cx.notify(); - }); - } - panel - }) + pub async fn load( + workspace: WeakView, + mut cx: AsyncWindowContext, + ) -> Result> { + let serialized_panel = cx + .background_executor() + .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) + .await + .map_err(|e| anyhow!("Failed to load project panel: {}", e)) + .log_err() + .flatten() + .map(|panel| serde_json::from_str::(&panel)) + .transpose() + .log_err() + .flatten(); + + workspace.update(&mut cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + if let Some(serialized_panel) = serialized_panel { + panel.update(cx, |panel, cx| { + panel.width = serialized_panel.width; + cx.notify(); + }); + } + panel }) } fn serialize(&mut self, cx: &mut ViewContext) { let width = self.width; - self.pending_serialization = cx.background().spawn( + self.pending_serialization = cx.background_executor().spawn( async move { KEY_VALUE_STORE .write_kvp( @@ -386,12 +357,19 @@ impl ProjectPanel { ); } + fn focus_in(&mut self, cx: &mut ViewContext) { + if !self.focus_handle.contains_focused(cx) { + cx.emit(Event::Focus); + } + } + fn deploy_context_menu( &mut self, - position: Vector2F, + position: Point, entry_id: ProjectEntryId, cx: &mut ViewContext, ) { + let this = cx.view().clone(); let project = self.project.read(cx); let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { @@ -405,61 +383,74 @@ impl ProjectPanel { entry_id, }); - let mut menu_entries = Vec::new(); if let Some((worktree, entry)) = self.selected_entry(cx) { let is_root = Some(entry) == worktree.root_entry(); - if !project.is_remote() { - menu_entries.push(ContextMenuItem::action( - "Add Folder to Project", - workspace::AddFolderToProject, - )); - if is_root { - let project = self.project.clone(); - menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { - project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); - })); - } - } - menu_entries.push(ContextMenuItem::action("New File", NewFile)); - menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); - menu_entries.push(ContextMenuItem::Separator); - menu_entries.push(ContextMenuItem::action("Cut", Cut)); - menu_entries.push(ContextMenuItem::action("Copy", Copy)); - if let Some(clipboard_entry) = self.clipboard_entry { - if clipboard_entry.worktree_id() == worktree.id() { - menu_entries.push(ContextMenuItem::action("Paste", Paste)); - } - } - menu_entries.push(ContextMenuItem::Separator); - menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); - menu_entries.push(ContextMenuItem::action( - "Copy Relative Path", - CopyRelativePath, - )); + let is_dir = entry.is_dir(); + let worktree_id = worktree.id(); + let is_local = project.is_local(); - if entry.is_dir() { - menu_entries.push(ContextMenuItem::Separator); - } - menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder)); - if entry.is_dir() { - menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); - menu_entries.push(ContextMenuItem::action( - "Search Inside", - NewSearchInDirectory, - )); - } + let context_menu = ContextMenu::build(cx, |mut menu, cx| { + if is_local { + menu = menu.action( + "Add Folder to Project", + Box::new(workspace::AddFolderToProject), + ); + if is_root { + menu = menu.entry( + "Remove from Project", + None, + cx.handler_for(&this, move |this, cx| { + this.project.update(cx, |project, cx| { + project.remove_worktree(worktree_id, cx) + }); + }), + ); + } + } - menu_entries.push(ContextMenuItem::Separator); - menu_entries.push(ContextMenuItem::action("Rename", Rename)); - if !is_root { - menu_entries.push(ContextMenuItem::action("Delete", Delete)); - } + menu = menu + .action("New File", Box::new(NewFile)) + .action("New Folder", Box::new(NewDirectory)) + .separator() + .action("Cut", Box::new(Cut)) + .action("Copy", Box::new(Copy)); + + if let Some(clipboard_entry) = self.clipboard_entry { + if clipboard_entry.worktree_id() == worktree_id { + menu = menu.action("Paste", Box::new(Paste)); + } + } + + menu = menu + .separator() + .action("Copy Path", Box::new(CopyPath)) + .action("Copy Relative Path", Box::new(CopyRelativePath)) + .separator() + .action("Reveal in Finder", Box::new(RevealInFinder)); + + if is_dir { + menu = menu + .action("Open in Terminal", Box::new(OpenInTerminal)) + .action("Search Inside", Box::new(NewSearchInDirectory)) + } + + menu = menu.separator().action("Rename", Box::new(Rename)); + + if !is_root { + menu = menu.action("Delete", Box::new(Delete)); + } + + menu + }); + + cx.focus_view(&context_menu); + let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { + this.context_menu.take(); + cx.notify(); + }); + self.context_menu = Some((context_menu, position, subscription)); } - self.context_menu.update(cx, |menu, cx| { - menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - }); - cx.notify(); } @@ -545,7 +536,7 @@ impl ProjectPanel { } }); self.update_visible_entries(Some((worktree_id, entry_id)), cx); - cx.focus_self(); + cx.focus(&self.focus_handle); cx.notify(); } } @@ -576,27 +567,23 @@ impl ProjectPanel { } } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(task) = self.confirm_edit(cx) { - return Some(task); + task.detach_and_log_err(cx); } - - None } - fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) { if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_file() { self.open_entry(entry.id, true, cx); } } - - None } fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { let edit_state = self.edit_state.as_mut()?; - cx.focus_self(); + cx.focus(&self.focus_handle); let worktree_id = edit_state.worktree_id; let is_new_entry = edit_state.is_new_entry; @@ -671,7 +658,7 @@ impl ProjectPanel { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { self.edit_state = None; self.update_visible_entries(None, cx); - cx.focus_self(); + cx.focus(&self.focus_handle); cx.notify(); } @@ -745,9 +732,10 @@ impl ProjectPanel { is_dir, processing_filename: None, }); - self.filename_editor - .update(cx, |editor, cx| editor.clear(cx)); - cx.focus(&self.filename_editor); + self.filename_editor.update(cx, |editor, cx| { + editor.clear(cx); + editor.focus(cx); + }); self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); self.autoscroll(cx); cx.notify(); @@ -782,42 +770,47 @@ impl ProjectPanel { editor.set_text(file_name, cx); editor.change_selections(Some(Autoscroll::fit()), cx, |s| { s.select_ranges([0..selection_end]) - }) + }); + editor.focus(cx); }); - cx.focus(&self.filename_editor); self.update_visible_entries(None, cx); self.autoscroll(cx); cx.notify(); } } - cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { - drag_and_drop.cancel_dragging::(cx); - }) + // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { + // drag_and_drop.cancel_dragging::(cx); + // }) } } - fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { - let Selection { entry_id, .. } = self.selection?; - let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; - let file_name = path.file_name()?; + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + maybe!({ + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; - let mut answer = cx.prompt( - PromptLevel::Info, - &format!("Delete {file_name:?}?"), - &["Delete", "Cancel"], - ); - Some(cx.spawn(|this, mut cx| async move { - if answer.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 - })) + let answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + + cx.spawn(|this, mut cx| async move { + if answer.await != Ok(0) { + return Ok(()); + } + this.update(&mut cx, |this, cx| { + this.project + .update(cx, |project, cx| project.delete_entry(entry_id, cx)) + .ok_or_else(|| anyhow!("no such entry")) + })?? + .await + }) + .detach_and_log_err(cx); + Some(()) + }); } fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { @@ -869,7 +862,7 @@ impl ProjectPanel { fn autoscroll(&mut self, cx: &mut ViewContext) { if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { - self.list.scroll_to(ScrollTarget::Show(index)); + self.list.scroll_to_item(index); cx.notify(); } } @@ -894,8 +887,9 @@ impl ProjectPanel { } } - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { - if let Some((worktree, entry)) = self.selected_entry(cx) { + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + maybe!({ + let (worktree, entry) = self.selected_entry(cx)?; let clipboard_entry = self.clipboard_entry?; if clipboard_entry.worktree_id() != worktree.id() { return None; @@ -948,8 +942,9 @@ impl ProjectPanel { }) .detach_and_log_err(cx) } - } - None + + Some(()) + }); } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { @@ -976,24 +971,25 @@ impl ProjectPanel { } } - fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { - if let Some((worktree, entry)) = self.selected_entry(cx) { - let window = cx.window(); - let view_id = cx.view_id(); - let path = worktree.abs_path().join(&entry.path); + fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext) { + todo!() + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let window = cx.window(); + // let view_id = cx.view_id(); + // let path = worktree.abs_path().join(&entry.path); - cx.app_context() - .spawn(|mut cx| async move { - window.dispatch_action( - view_id, - &workspace::OpenTerminal { - working_directory: path, - }, - &mut cx, - ); - }) - .detach(); - } + // cx.app_context() + // .spawn(|mut cx| async move { + // window.dispatch_action( + // view_id, + // &workspace::OpenTerminal { + // working_directory: path, + // }, + // &mut cx, + // ); + // }) + // .detach(); + // } } pub fn new_search_in_directory( @@ -1003,9 +999,12 @@ impl ProjectPanel { ) { if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_dir() { - cx.emit(Event::NewSearchInDirectory { - dir_entry: entry.clone(), - }); + let entry = entry.clone(); + self.workspace + .update(cx, |workspace, cx| { + search::ProjectSearchView::new_search_in_directory(workspace, &entry, cx); + }) + .ok(); } } } @@ -1030,7 +1029,7 @@ impl ProjectPanel { new_path.push(entry_path.path.file_name()?); if new_path != entry_path.path.as_ref() { let task = project.rename_entry(entry_to_move, new_path, cx); - cx.foreground().spawn(task).detach_and_log_err(cx); + cx.foreground_executor().spawn(task).detach_and_log_err(cx); } Some(project.worktree_id_for_entry(destination, cx)?) @@ -1075,7 +1074,7 @@ impl ProjectPanel { fn selected_entry_handle<'a>( &self, cx: &'a AppContext, - ) -> Option<(ModelHandle, &'a project::Entry)> { + ) -> Option<(Model, &'a project::Entry)> { let selection = self.selection?; let project = self.project.read(cx); let worktree = project.worktree_for_id(selection.worktree_id, cx)?; @@ -1262,7 +1261,7 @@ impl ProjectPanel { let end_ix = range.end.min(ix + visible_worktree_entries.len()); let (git_status_setting, show_file_icons, show_folder_icons) = { - let settings = settings::get::(cx); + let settings = ProjectPanelSettings::get_global(cx); ( settings.git_status, settings.file_icons, @@ -1285,16 +1284,16 @@ impl ProjectPanel { let icon = match entry.kind { EntryKind::File(_) => { if show_file_icons { - Some(FileAssociations::get_icon(&entry.path, cx)) + FileAssociations::get_icon(&entry.path, cx) } else { None } } _ => { if show_folder_icons { - Some(FileAssociations::get_folder_icon(is_expanded, cx)) + FileAssociations::get_folder_icon(is_expanded, cx) } else { - Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + FileAssociations::get_chevron_icon(is_expanded, cx) } } }; @@ -1351,193 +1350,115 @@ impl ProjectPanel { } } - fn render_entry_visual_element( - details: &EntryDetails, - editor: Option<&ViewHandle>, - padding: f32, - row_container_style: ContainerStyle, - style: &ProjectPanelEntry, - cx: &mut ViewContext, - ) -> AnyElement { + fn render_entry( + &self, + entry_id: ProjectEntryId, + details: EntryDetails, + cx: &mut ViewContext, + ) -> Stateful
{ + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); + let width = self.width.unwrap_or(px(0.)); - let mut filename_text_style = style.text.clone(); - filename_text_style.color = details + let filename_text_color = details .git_status .as_ref() .map(|status| match status { - GitFileStatus::Added => style.status.git.inserted, - GitFileStatus::Modified => style.status.git.modified, - GitFileStatus::Conflict => style.status.git.conflict, + GitFileStatus::Added => Color::Created, + GitFileStatus::Modified => Color::Modified, + GitFileStatus::Conflict => Color::Conflict, }) - .unwrap_or(style.text.color); + .unwrap_or(if is_selected { + Color::Default + } else { + Color::Muted + }); - Flex::row() - .with_child(if let Some(icon) = &details.icon { - Svg::new(icon.to_string()) - .with_color(style.icon_color) - .constrained() - .with_max_width(style.icon_size) - .with_max_height(style.icon_size) - .aligned() - .constrained() - .with_width(style.icon_size) - } else { - Empty::new() - .constrained() - .with_max_width(style.icon_size) - .with_max_height(style.icon_size) - .aligned() - .constrained() - .with_width(style.icon_size) + let file_name = details.filename.clone(); + let icon = details.icon.clone(); + let depth = details.depth; + div() + .id(entry_id.to_proto() as usize) + .on_drag(entry_id, move |entry_id, cx| { + cx.new_view(|_| DraggedProjectEntryView { + details: details.clone(), + width, + entry_id: *entry_id, + }) }) - .with_child(if show_editor && editor.is_some() { - ChildView::new(editor.as_ref().unwrap(), cx) - .contained() - .with_margin_left(style.icon_spacing) - .aligned() - .left() - .flex(1.0, true) - .into_any() - } else { - Label::new(details.filename.clone(), filename_text_style) - .contained() - .with_margin_left(style.icon_spacing) - .aligned() - .left() - .into_any() + .drag_over::(|style| { + style.bg(cx.theme().colors().drop_target_background) }) - .constrained() - .with_height(style.height) - .contained() - .with_style(row_container_style) - .with_padding_left(padding) - .into_any_named("project panel entry visual element") + .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| { + this.move_entry(*dragged_id, entry_id, kind.is_file(), cx); + })) + .child( + ListItem::new(entry_id.to_proto() as usize) + .indent_level(depth) + .indent_step_size(px(settings.indent_size)) + .selected(is_selected) + .child(if let Some(icon) = &icon { + div().child(IconElement::from_path(icon.to_string()).color(Color::Muted)) + } else { + div() + }) + .child( + if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { + div().h_full().w_full().child(editor.clone()) + } else { + div().child(Label::new(file_name).color(filename_text_color)) + } + .ml_1(), + ) + .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { + if event.down.button == MouseButton::Right { + return; + } + if !show_editor { + if kind.is_dir() { + this.toggle_expanded(entry_id, cx); + } else { + if event.down.modifiers.command { + this.split_entry(entry_id, cx); + } else { + this.open_entry(entry_id, event.up.click_count > 1, cx); + } + } + } + })) + .on_secondary_mouse_down(cx.listener( + move |this, event: &MouseDownEvent, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + }, + )), + ) } - fn render_entry( - entry_id: ProjectEntryId, - details: EntryDetails, - editor: &ViewHandle, - dragged_entry_destination: &mut Option>, - theme: &theme::ProjectPanel, - cx: &mut ViewContext, - ) -> AnyElement { - let kind = details.kind; - let path = details.path.clone(); - let settings = settings::get::(cx); - let padding = theme.container.padding.left + details.depth as f32 * settings.indent_size; + fn dispatch_context(&self, cx: &ViewContext) -> KeyContext { + let mut dispatch_context = KeyContext::default(); + dispatch_context.add("ProjectPanel"); + dispatch_context.add("menu"); - let entry_style = if details.is_cut { - &theme.cut_entry - } else if details.is_ignored { - &theme.ignored_entry + let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) { + "editing" } else { - &theme.entry + "not_editing" }; - let show_editor = details.is_editing && !details.is_processing; - - MouseEventHandler::new::(entry_id.to_usize(), cx, |state, cx| { - let mut style = entry_style - .in_state(details.is_selected) - .style_for(state) - .clone(); - - if cx - .global::>() - .currently_dragged::(cx.window()) - .is_some() - && dragged_entry_destination - .as_ref() - .filter(|destination| details.path.starts_with(destination)) - .is_some() - { - style = entry_style.active_state().default.clone(); - } - - let row_container_style = if show_editor { - theme.filename_editor.container - } else { - style.container - }; - - Self::render_entry_visual_element( - &details, - Some(editor), - padding, - row_container_style, - &style, - cx, - ) - }) - .on_click(MouseButton::Left, move |event, this, cx| { - if !show_editor { - if kind.is_dir() { - this.toggle_expanded(entry_id, cx); - } else { - if event.cmd { - this.split_entry(entry_id, cx); - } else if !event.cmd { - this.open_entry(entry_id, event.click_count > 1, cx); - } - } - } - }) - .on_down(MouseButton::Right, move |event, this, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - }) - .on_up(MouseButton::Left, move |_, this, cx| { - if let Some((_, dragged_entry)) = cx - .global::>() - .currently_dragged::(cx.window()) - { - this.move_entry( - *dragged_entry, - entry_id, - matches!(details.kind, EntryKind::File(_)), - cx, - ); - } - }) - .on_move(move |_, this, cx| { - if cx - .global::>() - .currently_dragged::(cx.window()) - .is_some() - { - this.dragged_entry_destination = if matches!(kind, EntryKind::File(_)) { - path.parent().map(|parent| Arc::from(parent)) - } else { - Some(path.clone()) - }; - } - }) - .as_draggable(entry_id, { - let row_container_style = theme.dragged_entry.container; - - move |_, _, cx: &mut ViewContext| { - let theme = theme::current(cx).clone(); - Self::render_entry_visual_element( - &details, - None, - padding, - row_container_style, - &theme.project_panel.dragged_entry, - cx, - ) - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .into_any_named("project panel entry") + dispatch_context.add(identifier); + dispatch_context } fn reveal_entry( &mut self, - project: ModelHandle, + project: Model, entry_id: ProjectEntryId, skip_ignored: bool, - cx: &mut ViewContext<'_, '_, ProjectPanel>, + cx: &mut ViewContext<'_, ProjectPanel>, ) { if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { let worktree = worktree.read(cx); @@ -1558,139 +1479,101 @@ impl ProjectPanel { } } -impl View for ProjectPanel { - fn ui_name() -> &'static str { - "ProjectPanel" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> gpui::AnyElement { - enum ProjectPanel {} - let theme = &theme::current(cx).project_panel; - let mut container_style = theme.container; - let padding = std::mem::take(&mut container_style.padding); - let last_worktree_root_id = self.last_worktree_root_id; - +impl Render for ProjectPanel { + fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { let has_worktree = self.visible_entries.len() != 0; if has_worktree { - Stack::new() - .with_child( - MouseEventHandler::new::(0, cx, |_, cx| { - UniformList::new( - self.list.clone(), - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - cx, - move |this, range, items, cx| { - let theme = theme::current(cx).clone(); - let mut dragged_entry_destination = - this.dragged_entry_destination.clone(); + div() + .id("project-panel") + .size_full() + .relative() + .key_context(self.dispatch_context(cx)) + .on_action(cx.listener(Self::select_next)) + .on_action(cx.listener(Self::select_prev)) + .on_action(cx.listener(Self::expand_selected_entry)) + .on_action(cx.listener(Self::collapse_selected_entry)) + .on_action(cx.listener(Self::collapse_all_entries)) + .on_action(cx.listener(Self::new_file)) + .on_action(cx.listener(Self::new_directory)) + .on_action(cx.listener(Self::rename)) + .on_action(cx.listener(Self::delete)) + .on_action(cx.listener(Self::confirm)) + .on_action(cx.listener(Self::open_file)) + .on_action(cx.listener(Self::cancel)) + .on_action(cx.listener(Self::cut)) + .on_action(cx.listener(Self::copy)) + .on_action(cx.listener(Self::copy_path)) + .on_action(cx.listener(Self::copy_relative_path)) + .on_action(cx.listener(Self::paste)) + .on_action(cx.listener(Self::reveal_in_finder)) + .on_action(cx.listener(Self::open_in_terminal)) + .on_action(cx.listener(Self::new_search_in_directory)) + .track_focus(&self.focus_handle) + .child( + uniform_list( + cx.view().clone(), + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + { + |this, range, cx| { + let mut items = Vec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - &mut dragged_entry_destination, - &theme.project_panel, - cx, - )); + items.push(this.render_entry(id, details, cx)); }); - this.dragged_entry_destination = dragged_entry_destination; - }, - ) - .with_padding_top(padding.top) - .with_padding_bottom(padding.bottom) - .contained() - .with_style(container_style) - .expanded() - }) - .on_down(MouseButton::Right, move |event, this, cx| { - // When deploying the context menu anywhere below the last project entry, - // act as if the user clicked the root of the last worktree. - if let Some(entry_id) = last_worktree_root_id { - this.deploy_context_menu(event.position, entry_id, cx); - } - }), + items + } + }, + ) + .size_full() + .track_scroll(self.list.clone()), ) - .with_child(ChildView::new(&self.context_menu, cx)) - .into_any_named("project panel") + .children(self.context_menu.as_ref().map(|(menu, position, _)| { + overlay() + .position(*position) + .anchor(gpui::AnchorCorner::TopLeft) + .child(menu.clone()) + })) } else { - Flex::column() - .with_child( - MouseEventHandler::new::(2, cx, { - let button_style = theme.open_project_button.clone(); - let context_menu_item_style = theme::current(cx).context_menu.item.clone(); - move |state, cx| { - let button_style = button_style.style_for(state).clone(); - let context_menu_item = context_menu_item_style - .active_state() - .style_for(state) - .clone(); - - theme::ui::keystroke_label( - "Open a project", - &button_style, - &context_menu_item.keystroke, - Box::new(workspace::Open), - cx, - ) - } - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - if let Some(task) = workspace.open(&Default::default(), cx) { - task.detach_and_log_err(cx); - } - }) - } - }) - .with_cursor_style(CursorStyle::PointingHand), - ) - .contained() - .with_style(container_style) - .into_any_named("empty project panel") + v_stack() + .id("empty-project_panel") + .track_focus(&self.focus_handle) } } - - fn update_keymap_context(&self, keymap: &mut KeymapContext, cx: &AppContext) { - Self::reset_to_default_keymap_context(keymap); - keymap.add_identifier("menu"); - - if let Some(window) = cx.active_window() { - window.read_with(cx, |cx| { - let identifier = if self.filename_editor.is_focused(cx) { - "editing" - } else { - "not_editing" - }; - - keymap.add_identifier(identifier); - }); - } - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.has_focus { - self.has_focus = true; - cx.emit(Event::Focus); - } - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { - self.has_focus = false; - } } -impl Entity for ProjectPanel { - type Event = Event; +impl Render for DraggedProjectEntryView { + fn render(&mut self, cx: &mut ViewContext) -> impl Element { + let settings = ProjectPanelSettings::get_global(cx); + let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); + h_stack() + .font(ui_font) + .bg(cx.theme().colors().background) + .w(self.width) + .child( + ListItem::new(self.entry_id.to_proto() as usize) + .indent_level(self.details.depth) + .indent_step_size(px(settings.indent_size)) + .child(if let Some(icon) = &self.details.icon { + div().child(IconElement::from_path(icon.to_string())) + } else { + div() + }) + .child(Label::new(self.details.filename.clone())), + ) + } } -impl workspace::dock::Panel for ProjectPanel { +impl EventEmitter for ProjectPanel {} + +impl EventEmitter for ProjectPanel {} + +impl Panel for ProjectPanel { fn position(&self, cx: &WindowContext) -> DockPosition { - match settings::get::(cx).dock { + match ProjectPanelSettings::get_global(cx).dock { ProjectPanelDockPosition::Left => DockPosition::Left, ProjectPanelDockPosition::Right => DockPosition::Right, } @@ -1714,35 +1597,37 @@ impl workspace::dock::Panel for ProjectPanel { ); } - fn size(&self, cx: &WindowContext) -> f32 { + fn size(&self, cx: &WindowContext) -> Pixels { self.width - .unwrap_or_else(|| settings::get::(cx).default_width) + .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) } - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { self.width = size; self.serialize(cx); cx.notify(); } - fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { - Some("icons/project.svg") + fn icon(&self, _: &WindowContext) -> Option { + Some(ui::Icon::FileTree) } - fn icon_tooltip(&self) -> (String, Option>) { - ("Project Panel".into(), Some(Box::new(ToggleFocus))) + fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { + Some("Project Panel") } - fn should_change_position_on_event(event: &Self::Event) -> bool { - matches!(event, Event::DockPositionChanged) + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) } - fn has_focus(&self, _: &WindowContext) -> bool { - self.has_focus + fn persistent_name() -> &'static str { + "Project Panel" } +} - fn is_focus_event(event: &Self::Event) -> bool { - matches!(event, Event::Focus) +impl FocusableView for ProjectPanel { + fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { + self.focus_handle.clone() } } @@ -1770,7 +1655,7 @@ impl ClipboardEntry { #[cfg(test)] mod tests { use super::*; - use gpui::{AnyWindowHandle, TestAppContext, ViewHandle, WindowHandle}; + use gpui::{TestAppContext, View, VisualTestContext, WindowHandle}; use pretty_assertions::assert_eq; use project::{project_settings::ProjectSettings, FakeFs}; use serde_json::json; @@ -1778,15 +1663,14 @@ mod tests { use std::{ collections::HashSet, path::{Path, PathBuf}, - sync::atomic::{self, AtomicUsize}, }; - use workspace::{pane, AppState}; + use workspace::AppState; #[gpui::test] async fn test_visible_list(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/root1", json!({ @@ -1824,10 +1708,11 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), &[ @@ -1876,7 +1761,7 @@ mod tests { async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { init_test(cx); cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |project_settings| { project_settings.file_scan_exclusions = Some(vec!["**/.git".to_string(), "**/4/**".to_string()]); @@ -1884,7 +1769,7 @@ mod tests { }); }); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/root1", json!({ @@ -1922,10 +1807,11 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), &[ @@ -1993,7 +1879,7 @@ mod tests { async fn test_editing_files(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/root1", json!({ @@ -2031,9 +1917,16 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + workspace.add_panel(panel.clone(), cx); + workspace.toggle_dock(panel.read(cx).position(cx), cx); + panel + }) + .unwrap(); select_path(&panel, "root1", cx); assert_eq!( @@ -2054,9 +1947,8 @@ mod tests { // Add a file with the root folder selected. The filename editor is placed // before the first file in the root folder. panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - window.read_with(cx, |cx| { - let panel = panel.read(cx); - assert!(panel.filename_editor.is_focused(cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2078,7 +1970,7 @@ mod tests { panel .filename_editor .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); - panel.confirm(&Confirm, cx).unwrap() + panel.confirm_edit(cx).unwrap() }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2136,7 +2028,7 @@ mod tests { panel .filename_editor .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); - panel.confirm(&Confirm, cx).unwrap() + panel.confirm_edit(cx).unwrap() }) .await .unwrap(); @@ -2184,7 +2076,7 @@ mod tests { editor.set_text("a-different-filename.tar.gz", cx) }); - panel.confirm(&Confirm, cx).unwrap() + panel.confirm_edit(cx).unwrap() }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2242,7 +2134,7 @@ mod tests { assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); let file_name_selection = &file_name_selections[0]; assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); - assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); + assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot.."); }); panel.cancel(&Cancel, cx) @@ -2269,7 +2161,7 @@ mod tests { panel .filename_editor .update(cx, |editor, cx| editor.set_text("new-dir", cx)); - panel.confirm(&Confirm, cx).unwrap() + panel.confirm_edit(cx).unwrap() }); panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); assert_eq!( @@ -2323,7 +2215,7 @@ mod tests { ); // Dismiss the rename editor when it loses focus. - workspace.update(cx, |_, cx| cx.focus_self()); + workspace.update(cx, |_, cx| cx.blur()).unwrap(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2341,11 +2233,11 @@ mod tests { ); } - #[gpui::test(iterations = 30)] + #[gpui::test(iterations = 10)] async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/root1", json!({ @@ -2383,9 +2275,16 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + workspace.add_panel(panel.clone(), cx); + workspace.toggle_dock(panel.read(cx).position(cx), cx); + panel + }) + .unwrap(); select_path(&panel, "root1", cx); assert_eq!( @@ -2406,9 +2305,8 @@ mod tests { // Add a file with the root folder selected. The filename editor is placed // before the first file in the root folder. panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - window.read_with(cx, |cx| { - let panel = panel.read(cx); - assert!(panel.filename_editor.is_focused(cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2430,7 +2328,7 @@ mod tests { panel.filename_editor.update(cx, |editor, cx| { editor.set_text("/bdir1/dir2/the-new-filename", cx) }); - panel.confirm(&Confirm, cx).unwrap() + panel.confirm_edit(cx).unwrap() }); assert_eq!( @@ -2473,7 +2371,7 @@ mod tests { async fn test_copy_paste(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/root1", json!({ @@ -2484,10 +2382,11 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); panel.update(cx, |panel, cx| { panel.select_next(&Default::default(), cx); @@ -2510,7 +2409,7 @@ mod tests { panel.copy(&Default::default(), cx); panel.paste(&Default::default(), cx); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -2526,7 +2425,7 @@ mod tests { panel.update(cx, |panel, cx| { panel.paste(&Default::default(), cx); }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..50, cx), @@ -2545,7 +2444,7 @@ mod tests { async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/src", json!({ @@ -2559,14 +2458,16 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); toggle_expand_dir(&panel, "src/test", cx); select_path(&panel, "src/test/first.rs", cx); panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2577,9 +2478,9 @@ mod tests { " third.rs" ] ); - ensure_single_file_is_opened(window, "test/first.rs", cx); + ensure_single_file_is_opened(&workspace, "test/first.rs", cx); - submit_deletion(window.into(), &panel, cx); + submit_deletion(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2590,11 +2491,11 @@ mod tests { ], "Project panel should have no deleted file, no other file is selected in it" ); - ensure_no_open_items_and_panes(window.into(), &workspace, cx); + ensure_no_open_items_and_panes(&workspace, cx); select_path(&panel, "src/test/second.rs", cx); panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2604,38 +2505,39 @@ mod tests { " third.rs" ] ); - ensure_single_file_is_opened(window, "test/second.rs", cx); + ensure_single_file_is_opened(&workspace, "test/second.rs", cx); - window.update(cx, |cx| { - let active_items = workspace - .read(cx) - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let open_editor = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an editor"); - open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); - }); - submit_deletion(window.into(), &panel, cx); + workspace + .update(cx, |workspace, cx| { + let active_items = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()) + .collect::>(); + assert_eq!(active_items.len(), 1); + let open_editor = active_items + .into_iter() + .next() + .unwrap() + .downcast::() + .expect("Open item should be an editor"); + open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); + }) + .unwrap(); + submit_deletion(&panel, cx); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &["v src", " v test", " third.rs"], "Project panel should have no deleted file, with one last file remaining" ); - ensure_no_open_items_and_panes(window.into(), &workspace, cx); + ensure_no_open_items_and_panes(&workspace, cx); } #[gpui::test] async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/src", json!({ @@ -2649,52 +2551,74 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let workspace = window.root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + workspace.add_panel(panel.clone(), cx); + workspace.toggle_dock(panel.read(cx).position(cx), cx); + panel + }) + .unwrap(); select_path(&panel, "src/", cx); panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src <== selected", " > test"] + &[ + // + "v src <== selected", + " > test" + ] ); panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); - window.read_with(cx, |cx| { - let panel = panel.read(cx); - assert!(panel.filename_editor.is_focused(cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " > [EDITOR: ''] <== selected", " > test"] + &[ + // + "v src", + " > [EDITOR: ''] <== selected", + " > test" + ] ); panel.update(cx, |panel, cx| { panel .filename_editor .update(cx, |editor, cx| editor.set_text("test", cx)); assert!( - panel.confirm(&Confirm, cx).is_none(), + panel.confirm_edit(cx).is_none(), "Should not allow to confirm on conflicting new directory name" ) }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " > test"], + &[ + // + "v src", + " > test" + ], "File list should be unchanged after failed folder create confirmation" ); select_path(&panel, "src/test/", cx); panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " > test <== selected"] + &[ + // + "v src", + " > test <== selected" + ] ); panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - window.read_with(cx, |cx| { - let panel = panel.read(cx); - assert!(panel.filename_editor.is_focused(cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2712,7 +2636,7 @@ mod tests { .filename_editor .update(cx, |editor, cx| editor.set_text("first.rs", cx)); assert!( - panel.confirm(&Confirm, cx).is_none(), + panel.confirm_edit(cx).is_none(), "Should not allow to confirm on conflicting new file name" ) }); @@ -2730,7 +2654,7 @@ mod tests { select_path(&panel, "src/test/first.rs", cx); panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2742,9 +2666,8 @@ mod tests { ], ); panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); - window.read_with(cx, |cx| { - let panel = panel.read(cx); - assert!(panel.filename_editor.is_focused(cx)); + panel.update(cx, |panel, cx| { + assert!(panel.filename_editor.read(cx).is_focused(cx)); }); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2761,7 +2684,7 @@ mod tests { .filename_editor .update(cx, |editor, cx| editor.set_text("second.rs", cx)); assert!( - panel.confirm(&Confirm, cx).is_none(), + panel.confirm_edit(cx).is_none(), "Should not allow to confirm on conflicting file rename" ) }); @@ -2778,90 +2701,11 @@ mod tests { ); } - #[gpui::test] - async fn test_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.background()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); - - let new_search_events_count = Arc::new(AtomicUsize::new(0)); - let _subscription = panel.update(cx, |_, cx| { - let subcription_count = Arc::clone(&new_search_events_count); - cx.subscribe(&cx.handle(), move |_, _, event, _| { - if matches!(event, Event::NewSearchInDirectory { .. }) { - subcription_count.fetch_add(1, atomic::Ordering::SeqCst); - } - }) - }); - - toggle_expand_dir(&panel, "src/test", cx); - select_path(&panel, "src/test/first.rs", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.foreground().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ] - ); - panel.update(cx, |panel, cx| { - panel.new_search_in_directory(&NewSearchInDirectory, cx) - }); - assert_eq!( - new_search_events_count.load(atomic::Ordering::SeqCst), - 0, - "Should not trigger new search in directory when called on a file" - ); - - select_path(&panel, "src/test", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.foreground().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test <== selected", - " first.rs", - " second.rs", - " third.rs" - ] - ); - panel.update(cx, |panel, cx| { - panel.new_search_in_directory(&NewSearchInDirectory, cx) - }); - assert_eq!( - new_search_events_count.load(atomic::Ordering::SeqCst), - 1, - "Should trigger new search in directory when called on a directory" - ); - } - #[gpui::test] async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.insert_tree( "/project_root", json!({ @@ -2885,15 +2729,16 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); panel.update(cx, |panel, cx| { panel.collapse_all_entries(&CollapseAllEntries, cx) }); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &["v project_root", " > dir_1", " > dir_2",] @@ -2901,7 +2746,7 @@ mod tests { // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &[ @@ -2920,27 +2765,32 @@ mod tests { async fn test_new_file_move(cx: &mut gpui::TestAppContext) { init_test(cx); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.executor().clone()); fs.as_fake().insert_tree("/root", json!({})).await; let project = Project::test(fs, ["/root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); // Make a new buffer with no backing file - workspace.update(cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }); + workspace + .update(cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .unwrap(); // "Save as"" the buffer, creating a new backing file for it - let task = workspace.update(cx, |workspace, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, cx) - }); + let save_task = workspace + .update(cx, |workspace, cx| { + workspace.save_active_item(workspace::SaveIntent::Save, cx) + }) + .unwrap(); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); - task.await.unwrap(); + save_task.await.unwrap(); // Rename the file select_path(&panel, "root/new", cx); @@ -2954,13 +2804,9 @@ mod tests { .filename_editor .update(cx, |editor, cx| editor.set_text("newer", cx)); }); - panel - .update(cx, |panel, cx| panel.confirm(&Confirm, cx)) - .unwrap() - .await - .unwrap(); + panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), &["v root", " newer <== selected"] @@ -2970,10 +2816,11 @@ mod tests { .update(cx, |workspace, cx| { workspace.save_active_item(workspace::SaveIntent::Save, cx) }) + .unwrap() .await .unwrap(); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); // assert that saving the file doesn't restore "new" assert_eq!( visible_entries_as_strings(&panel, 0..10, cx), @@ -2985,7 +2832,7 @@ mod tests { async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |project_settings| { project_settings.file_scan_exclusions = Some(Vec::new()); }); @@ -2995,7 +2842,7 @@ mod tests { }) }); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/project_root", json!({ @@ -3021,10 +2868,11 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), @@ -3050,7 +2898,7 @@ mod tests { toggle_expand_dir(&panel, "project_root/dir_1", cx); toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3093,7 +2941,7 @@ mod tests { cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3108,7 +2956,7 @@ mod tests { } cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |project_panel_settings| { project_panel_settings.auto_reveal_entries = Some(true) }); @@ -3120,7 +2968,7 @@ mod tests { cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file))) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3142,7 +2990,7 @@ mod tests { cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file))) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3169,7 +3017,7 @@ mod tests { ))) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3194,7 +3042,7 @@ mod tests { cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3222,7 +3070,7 @@ mod tests { async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { init_test_with_editor(cx); cx.update(|cx| { - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |project_settings| { project_settings.file_scan_exclusions = Some(Vec::new()); }); @@ -3232,7 +3080,7 @@ mod tests { }) }); - let fs = FakeFs::new(cx.background()); + let fs = FakeFs::new(cx.background_executor.clone()); fs.insert_tree( "/project_root", json!({ @@ -3258,10 +3106,11 @@ mod tests { .await; let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx - .add_window(|cx| Workspace::test_new(project.clone(), cx)) - .root(cx); - let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let cx = &mut VisualTestContext::from_window(*workspace, cx); + let panel = workspace + .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) + .unwrap(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), @@ -3287,7 +3136,7 @@ mod tests { toggle_expand_dir(&panel, "project_root/dir_1", cx); toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3330,7 +3179,7 @@ mod tests { cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3349,7 +3198,7 @@ mod tests { cx.emit(project::Event::RevealInProjectPanel(dir_1_file)) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3371,7 +3220,7 @@ mod tests { cx.emit(project::Event::RevealInProjectPanel(dir_2_file)) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3396,7 +3245,7 @@ mod tests { cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) }) }); - cx.foreground().run_until_parked(); + cx.run_until_parked(); assert_eq!( visible_entries_as_strings(&panel, 0..20, cx), &[ @@ -3421,13 +3270,13 @@ mod tests { } fn toggle_expand_dir( - panel: &ViewHandle, + panel: &View, path: impl AsRef, - cx: &mut TestAppContext, + cx: &mut VisualTestContext, ) { let path = path.as_ref(); panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { + for worktree in panel.project.read(cx).worktrees().collect::>() { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; @@ -3439,36 +3288,32 @@ mod tests { }); } - fn select_path( - panel: &ViewHandle, - path: impl AsRef, - cx: &mut TestAppContext, - ) { + fn select_path(panel: &View, path: impl AsRef, cx: &mut VisualTestContext) { let path = path.as_ref(); panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { + for worktree in panel.project.read(cx).worktrees().collect::>() { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.selection = Some(Selection { + panel.selection = Some(crate::Selection { worktree_id: worktree.id(), entry_id, }); return; } } - panic!("no worktree for path {path:?}"); + panic!("no worktree for path {:?}", path); }); } fn find_project_entry( - panel: &ViewHandle, + panel: &View, path: impl AsRef, - cx: &mut TestAppContext, + cx: &mut VisualTestContext, ) -> Option { let path = path.as_ref(); panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees(cx).collect::>() { + for worktree in panel.project.read(cx).worktrees().collect::>() { let worktree = worktree.read(cx); if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { return worktree.entry_for_path(relative_path).map(|entry| entry.id); @@ -3479,9 +3324,9 @@ mod tests { } fn visible_entries_as_strings( - panel: &ViewHandle, + panel: &View, range: Range, - cx: &mut TestAppContext, + cx: &mut VisualTestContext, ) -> Vec { let mut result = Vec::new(); let mut project_entries = HashSet::new(); @@ -3531,11 +3376,11 @@ mod tests { } fn init_test(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); cx.update(|cx| { - cx.set_global(SettingsStore::test(cx)); + let settings_store = SettingsStore::test(cx); + cx.set_global(settings_store); init_settings(cx); - theme::init((), cx); + theme::init(theme::LoadThemes::JustBase, cx); language::init(cx); editor::init_settings(cx); crate::init((), cx); @@ -3543,7 +3388,7 @@ mod tests { client::init_settings(cx); Project::init_settings(cx); - cx.update_global::(|store, cx| { + cx.update_global::(|store, cx| { store.update_user_settings::(cx, |project_settings| { project_settings.file_scan_exclusions = Some(Vec::new()); }); @@ -3552,14 +3397,12 @@ mod tests { } fn init_test_with_editor(cx: &mut TestAppContext) { - cx.foreground().forbid_parking(); cx.update(|cx| { let app_state = AppState::test(cx); - theme::init((), cx); + theme::init(theme::LoadThemes::JustBase, cx); init_settings(cx); language::init(cx); editor::init(cx); - pane::init(cx); crate::init((), cx); workspace::init(app_state.clone(), cx); Project::init_settings(cx); @@ -3567,79 +3410,71 @@ mod tests { } fn ensure_single_file_is_opened( - window: WindowHandle, + window: &WindowHandle, expected_path: &str, cx: &mut TestAppContext, ) { - window.update_root(cx, |workspace, cx| { - let worktrees = workspace.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree_id = WorktreeId::from_usize(worktrees[0].id()); + window + .update(cx, |workspace, cx| { + let worktrees = workspace.worktrees(cx).collect::>(); + assert_eq!(worktrees.len(), 1); + let worktree_id = worktrees[0].read(cx).id(); - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert_eq!( - open_project_paths, - vec![ProjectPath { - worktree_id, - path: Arc::from(Path::new(expected_path)) - }], - "Should have opened file, selected in project panel" - ); - }); + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert_eq!( + open_project_paths, + vec![ProjectPath { + worktree_id, + path: Arc::from(Path::new(expected_path)) + }], + "Should have opened file, selected in project panel" + ); + }) + .unwrap(); } - fn submit_deletion( - window: AnyWindowHandle, - panel: &ViewHandle, - cx: &mut TestAppContext, - ) { + fn submit_deletion(panel: &View, cx: &mut VisualTestContext) { assert!( - !window.has_pending_prompt(cx), + !cx.has_pending_prompt(), "Should have no prompts before the deletion" ); - panel.update(cx, |panel, cx| { - panel - .delete(&Delete, cx) - .expect("Deletion start") - .detach_and_log_err(cx); - }); + panel.update(cx, |panel, cx| panel.delete(&Delete, cx)); assert!( - window.has_pending_prompt(cx), + cx.has_pending_prompt(), "Should have a prompt after the deletion" ); - window.simulate_prompt_answer(0, cx); + cx.simulate_prompt_answer(0); assert!( - !window.has_pending_prompt(cx), + !cx.has_pending_prompt(), "Should have no prompts after prompt was replied to" ); - cx.foreground().run_until_parked(); + cx.executor().run_until_parked(); } fn ensure_no_open_items_and_panes( - window: AnyWindowHandle, - workspace: &ViewHandle, - cx: &mut TestAppContext, + workspace: &WindowHandle, + cx: &mut VisualTestContext, ) { assert!( - !window.has_pending_prompt(cx), + !cx.has_pending_prompt(), "Should have no prompts after deletion operation closes the file" ); - window.read_with(cx, |cx| { - let open_project_paths = workspace - .read(cx) - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert!( - open_project_paths.is_empty(), - "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" - ); - }); + workspace + .read_with(cx, |workspace, cx| { + let open_project_paths = workspace + .panes() + .iter() + .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) + .collect::>(); + assert!( + open_project_paths.is_empty(), + "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" + ); + }) + .unwrap(); } } -// TODO - a workspace command? diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 836fe1d558..b9a87a1a03 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -1,7 +1,8 @@ use anyhow; +use gpui::Pixels; use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; -use settings::Setting; +use settings::Settings; #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "snake_case")] @@ -12,7 +13,7 @@ pub enum ProjectPanelDockPosition { #[derive(Deserialize, Debug)] pub struct ProjectPanelSettings { - pub default_width: f32, + pub default_width: Pixels, pub dock: ProjectPanelDockPosition, pub file_icons: bool, pub folder_icons: bool, @@ -32,7 +33,7 @@ pub struct ProjectPanelSettingsContent { pub auto_reveal_entries: Option, } -impl Setting for ProjectPanelSettings { +impl Settings for ProjectPanelSettings { const KEY: Option<&'static str> = Some("project_panel"); type FileContent = ProjectPanelSettingsContent; @@ -40,7 +41,7 @@ impl Setting for ProjectPanelSettings { fn load( default_value: &Self::FileContent, user_values: &[&Self::FileContent], - _: &gpui::AppContext, + _: &mut gpui::AppContext, ) -> anyhow::Result { Self::load_via_json_merge(default_value, user_values) } diff --git a/crates/project_panel2/Cargo.toml b/crates/project_panel2/Cargo.toml deleted file mode 100644 index d3ef1eb825..0000000000 --- a/crates/project_panel2/Cargo.toml +++ /dev/null @@ -1,41 +0,0 @@ -[package] -name = "project_panel2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/project_panel.rs" -doctest = false - -[dependencies] -collections = { path = "../collections" } -db = { path = "../db2", package = "db2" } -editor = { path = "../editor2", package = "editor2" } -gpui = { path = "../gpui2", package = "gpui2" } -menu = { path = "../menu2", package = "menu2" } -project = { path = "../project2", package = "project2" } -search = { package = "search2", path = "../search2" } -settings = { path = "../settings2", package = "settings2" } -theme = { path = "../theme2", package = "theme2" } -ui = { path = "../ui2", package = "ui2" } -util = { path = "../util" } -workspace = { path = "../workspace2", package = "workspace2" } -anyhow.workspace = true -postage.workspace = true -futures.workspace = true -serde.workspace = true -serde_derive.workspace = true -serde_json.workspace = true -schemars.workspace = true -smallvec.workspace = true -pretty_assertions.workspace = true -unicase = "2.6" - -[dev-dependencies] -client = { path = "../client2", package = "client2", features = ["test-support"] } -language = { path = "../language2", package = "language2", features = ["test-support"] } -editor = { path = "../editor2", package = "editor2", features = ["test-support"] } -gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } -workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } -serde_json.workspace = true diff --git a/crates/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs deleted file mode 100644 index 82aebe7913..0000000000 --- a/crates/project_panel2/src/file_associations.rs +++ /dev/null @@ -1,87 +0,0 @@ -use std::{path::Path, str, sync::Arc}; - -use collections::HashMap; - -use gpui::{AppContext, AssetSource}; -use serde_derive::Deserialize; -use util::{maybe, paths::PathExt}; - -#[derive(Deserialize, Debug)] -struct TypeConfig { - icon: Arc, -} - -#[derive(Deserialize, Debug)] -pub struct FileAssociations { - suffixes: HashMap, - types: HashMap, -} - -const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder"; -const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder"; -const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron"; -const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron"; -pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; - -pub fn init(assets: impl AssetSource, cx: &mut AppContext) { - cx.set_global(FileAssociations::new(assets)) -} - -impl FileAssociations { - pub fn new(assets: impl AssetSource) -> Self { - assets - .load("icons/file_icons/file_types.json") - .and_then(|file| { - serde_json::from_str::(str::from_utf8(&file).unwrap()) - .map_err(Into::into) - }) - .unwrap_or_else(|_| FileAssociations { - suffixes: HashMap::default(), - types: HashMap::default(), - }) - } - - pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { - let this = cx.has_global::().then(|| cx.global::())?; - - // FIXME: Associate a type with the languages and have the file's langauge - // override these associations - maybe!({ - let suffix = path.icon_suffix()?; - - this.suffixes - .get(suffix) - .and_then(|type_str| this.types.get(type_str)) - .map(|type_config| type_config.icon.clone()) - }) - .or_else(|| this.types.get("default").map(|config| config.icon.clone())) - } - - pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { - let this = cx.has_global::().then(|| cx.global::())?; - - let key = if expanded { - EXPANDED_DIRECTORY_TYPE - } else { - COLLAPSED_DIRECTORY_TYPE - }; - - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - } - - pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option> { - let this = cx.has_global::().then(|| cx.global::())?; - - let key = if expanded { - EXPANDED_CHEVRON_TYPE - } else { - COLLAPSED_CHEVRON_TYPE - }; - - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - } -} diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs deleted file mode 100644 index 8ab364e0c1..0000000000 --- a/crates/project_panel2/src/project_panel.rs +++ /dev/null @@ -1,3480 +0,0 @@ -pub mod file_associations; -mod project_panel_settings; -use settings::{Settings, SettingsStore}; - -use db::kvp::KEY_VALUE_STORE; -use editor::{scroll::autoscroll::Autoscroll, Cancel, Editor}; -use file_associations::FileAssociations; - -use anyhow::{anyhow, Result}; -use gpui::{ - actions, div, overlay, px, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, InteractiveElement, - KeyContext, Model, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, - Render, Stateful, Styled, Subscription, Task, UniformListScrollHandle, View, ViewContext, - VisualContext as _, WeakView, WindowContext, -}; -use menu::{Confirm, SelectNext, SelectPrev}; -use project::{ - repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, - Worktree, WorktreeId, -}; -use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; -use serde::{Deserialize, Serialize}; -use std::{ - cmp::Ordering, - collections::{hash_map, HashMap}, - ffi::OsStr, - ops::Range, - path::Path, - sync::Arc, -}; -use theme::ThemeSettings; -use ui::{prelude::*, v_stack, ContextMenu, IconElement, Label, ListItem}; -use unicase::UniCase; -use util::{maybe, ResultExt, TryFutureExt}; -use workspace::{ - dock::{DockPosition, Panel, PanelEvent}, - Workspace, -}; - -const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; -const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; - -pub struct ProjectPanel { - project: Model, - fs: Arc, - list: UniformListScrollHandle, - focus_handle: FocusHandle, - visible_entries: Vec<(WorktreeId, Vec)>, - last_worktree_root_id: Option, - expanded_dir_ids: HashMap>, - selection: Option, - context_menu: Option<(View, Point, Subscription)>, - edit_state: Option, - filename_editor: View, - clipboard_entry: Option, - _dragged_entry_destination: Option>, - workspace: WeakView, - width: Option, - pending_serialization: Task>, -} - -#[derive(Copy, Clone, Debug)] -struct Selection { - worktree_id: WorktreeId, - entry_id: ProjectEntryId, -} - -#[derive(Clone, Debug)] -struct EditState { - worktree_id: WorktreeId, - entry_id: ProjectEntryId, - is_new_entry: bool, - is_dir: bool, - processing_filename: Option, -} - -#[derive(Copy, Clone)] -pub enum ClipboardEntry { - Copied { - worktree_id: WorktreeId, - entry_id: ProjectEntryId, - }, - Cut { - worktree_id: WorktreeId, - entry_id: ProjectEntryId, - }, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct EntryDetails { - filename: String, - icon: Option>, - path: Arc, - depth: usize, - kind: EntryKind, - is_ignored: bool, - is_expanded: bool, - is_selected: bool, - is_editing: bool, - is_processing: bool, - is_cut: bool, - git_status: Option, -} - -actions!( - project_panel, - [ - ExpandSelectedEntry, - CollapseSelectedEntry, - CollapseAllEntries, - NewDirectory, - NewFile, - Copy, - CopyPath, - CopyRelativePath, - RevealInFinder, - OpenInTerminal, - Cut, - Paste, - Delete, - Rename, - Open, - ToggleFocus, - NewSearchInDirectory, - ] -); - -pub fn init_settings(cx: &mut AppContext) { - ProjectPanelSettings::register(cx); -} - -pub fn init(assets: impl AssetSource, cx: &mut AppContext) { - init_settings(cx); - file_associations::init(assets, cx); - - cx.observe_new_views(|workspace: &mut Workspace, _| { - workspace.register_action(|workspace, _: &ToggleFocus, cx| { - workspace.toggle_panel_focus::(cx); - }); - }) - .detach(); -} - -#[derive(Debug)] -pub enum Event { - OpenedEntry { - entry_id: ProjectEntryId, - focus_opened_item: bool, - }, - SplitEntry { - entry_id: ProjectEntryId, - }, - Focus, -} - -#[derive(Serialize, Deserialize)] -struct SerializedProjectPanel { - width: Option, -} - -struct DraggedProjectEntryView { - entry_id: ProjectEntryId, - details: EntryDetails, - width: Pixels, -} - -impl ProjectPanel { - fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { - let project = workspace.project().clone(); - let project_panel = cx.new_view(|cx: &mut ViewContext| { - cx.observe(&project, |this, _, cx| { - this.update_visible_entries(None, cx); - cx.notify(); - }) - .detach(); - let focus_handle = cx.focus_handle(); - - cx.on_focus(&focus_handle, Self::focus_in).detach(); - - cx.subscribe(&project, |this, project, event, cx| match event { - project::Event::ActiveEntryChanged(Some(entry_id)) => { - if ProjectPanelSettings::get_global(cx).auto_reveal_entries { - this.reveal_entry(project, *entry_id, true, cx); - } - } - project::Event::RevealInProjectPanel(entry_id) => { - this.reveal_entry(project, *entry_id, false, cx); - cx.emit(PanelEvent::Activate); - } - project::Event::ActivateProjectPanel => { - cx.emit(PanelEvent::Activate); - } - project::Event::WorktreeRemoved(id) => { - this.expanded_dir_ids.remove(id); - this.update_visible_entries(None, cx); - cx.notify(); - } - _ => {} - }) - .detach(); - - let filename_editor = cx.new_view(|cx| Editor::single_line(cx)); - - cx.subscribe(&filename_editor, |this, _, event, cx| match event { - editor::EditorEvent::BufferEdited - | editor::EditorEvent::SelectionsChanged { .. } => { - this.autoscroll(cx); - } - editor::EditorEvent::Blurred => { - if this - .edit_state - .as_ref() - .map_or(false, |state| state.processing_filename.is_none()) - { - this.edit_state = None; - this.update_visible_entries(None, cx); - } - } - _ => {} - }) - .detach(); - - // cx.observe_global::(|_, cx| { - // cx.notify(); - // }) - // .detach(); - - let mut this = Self { - project: project.clone(), - fs: workspace.app_state().fs.clone(), - list: UniformListScrollHandle::new(), - focus_handle, - visible_entries: Default::default(), - last_worktree_root_id: Default::default(), - expanded_dir_ids: Default::default(), - selection: None, - edit_state: None, - context_menu: None, - filename_editor, - clipboard_entry: None, - _dragged_entry_destination: None, - workspace: workspace.weak_handle(), - width: None, - pending_serialization: Task::ready(None), - }; - this.update_visible_entries(None, cx); - - // Update the dock position when the setting changes. - let mut old_dock_position = this.position(cx); - ProjectPanelSettings::register(cx); - cx.observe_global::(move |this, cx| { - let new_dock_position = this.position(cx); - if new_dock_position != old_dock_position { - old_dock_position = new_dock_position; - cx.emit(PanelEvent::ChangePosition); - } - }) - .detach(); - - this - }); - - cx.subscribe(&project_panel, { - let project_panel = project_panel.downgrade(); - move |workspace, _, event, cx| match event { - &Event::OpenedEntry { - entry_id, - focus_opened_item, - } => { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { - workspace - .open_path( - ProjectPath { - worktree_id: worktree.read(cx).id(), - path: entry.path.clone(), - }, - None, - focus_opened_item, - cx, - ) - .detach_and_log_err(cx); - if !focus_opened_item { - if let Some(project_panel) = project_panel.upgrade() { - let focus_handle = project_panel.read(cx).focus_handle.clone(); - cx.focus(&focus_handle); - } - } - } - } - } - &Event::SplitEntry { entry_id } => { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - if let Some(_entry) = worktree.read(cx).entry_for_id(entry_id) { - // workspace - // .split_path( - // ProjectPath { - // worktree_id: worktree.read(cx).id(), - // path: entry.path.clone(), - // }, - // cx, - // ) - // .detach_and_log_err(cx); - } - } - } - _ => {} - } - }) - .detach(); - - project_panel - } - - pub async fn load( - workspace: WeakView, - mut cx: AsyncWindowContext, - ) -> Result> { - let serialized_panel = cx - .background_executor() - .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) - .await - .map_err(|e| anyhow!("Failed to load project panel: {}", e)) - .log_err() - .flatten() - .map(|panel| serde_json::from_str::(&panel)) - .transpose() - .log_err() - .flatten(); - - workspace.update(&mut cx, |workspace, cx| { - let panel = ProjectPanel::new(workspace, cx); - if let Some(serialized_panel) = serialized_panel { - panel.update(cx, |panel, cx| { - panel.width = serialized_panel.width; - cx.notify(); - }); - } - panel - }) - } - - fn serialize(&mut self, cx: &mut ViewContext) { - let width = self.width; - self.pending_serialization = cx.background_executor().spawn( - async move { - KEY_VALUE_STORE - .write_kvp( - PROJECT_PANEL_KEY.into(), - serde_json::to_string(&SerializedProjectPanel { width })?, - ) - .await?; - anyhow::Ok(()) - } - .log_err(), - ); - } - - fn focus_in(&mut self, cx: &mut ViewContext) { - if !self.focus_handle.contains_focused(cx) { - cx.emit(Event::Focus); - } - } - - fn deploy_context_menu( - &mut self, - position: Point, - entry_id: ProjectEntryId, - cx: &mut ViewContext, - ) { - let this = cx.view().clone(); - let project = self.project.read(cx); - - let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { - id - } else { - return; - }; - - self.selection = Some(Selection { - worktree_id, - entry_id, - }); - - if let Some((worktree, entry)) = self.selected_entry(cx) { - let is_root = Some(entry) == worktree.root_entry(); - let is_dir = entry.is_dir(); - let worktree_id = worktree.id(); - let is_local = project.is_local(); - - let context_menu = ContextMenu::build(cx, |mut menu, cx| { - if is_local { - menu = menu.action( - "Add Folder to Project", - Box::new(workspace::AddFolderToProject), - ); - if is_root { - menu = menu.entry( - "Remove from Project", - None, - cx.handler_for(&this, move |this, cx| { - this.project.update(cx, |project, cx| { - project.remove_worktree(worktree_id, cx) - }); - }), - ); - } - } - - menu = menu - .action("New File", Box::new(NewFile)) - .action("New Folder", Box::new(NewDirectory)) - .separator() - .action("Cut", Box::new(Cut)) - .action("Copy", Box::new(Copy)); - - if let Some(clipboard_entry) = self.clipboard_entry { - if clipboard_entry.worktree_id() == worktree_id { - menu = menu.action("Paste", Box::new(Paste)); - } - } - - menu = menu - .separator() - .action("Copy Path", Box::new(CopyPath)) - .action("Copy Relative Path", Box::new(CopyRelativePath)) - .separator() - .action("Reveal in Finder", Box::new(RevealInFinder)); - - if is_dir { - menu = menu - .action("Open in Terminal", Box::new(OpenInTerminal)) - .action("Search Inside", Box::new(NewSearchInDirectory)) - } - - menu = menu.separator().action("Rename", Box::new(Rename)); - - if !is_root { - menu = menu.action("Delete", Box::new(Delete)); - } - - menu - }); - - cx.focus_view(&context_menu); - let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { - this.context_menu.take(); - cx.notify(); - }); - self.context_menu = Some((context_menu, position, subscription)); - } - - cx.notify(); - } - - fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { - if let Some((worktree, entry)) = self.selected_entry(cx) { - if entry.is_dir() { - let worktree_id = worktree.id(); - let entry_id = entry.id; - let expanded_dir_ids = - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - expanded_dir_ids - } else { - return; - }; - - match expanded_dir_ids.binary_search(&entry_id) { - Ok(_) => self.select_next(&SelectNext, cx), - Err(ix) => { - self.project.update(cx, |project, cx| { - project.expand_entry(worktree_id, entry_id, cx); - }); - - expanded_dir_ids.insert(ix, entry_id); - self.update_visible_entries(None, cx); - cx.notify(); - } - } - } - } - } - - fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { - if let Some((worktree, mut entry)) = self.selected_entry(cx) { - let worktree_id = worktree.id(); - let expanded_dir_ids = - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - expanded_dir_ids - } else { - return; - }; - - loop { - let entry_id = entry.id; - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - cx.notify(); - break; - } - Err(_) => { - if let Some(parent_entry) = - entry.path.parent().and_then(|p| worktree.entry_for_path(p)) - { - entry = parent_entry; - } else { - break; - } - } - } - } - } - } - - pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { - self.expanded_dir_ids.clear(); - self.update_visible_entries(None, cx); - cx.notify(); - } - - fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { - if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { - if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { - self.project.update(cx, |project, cx| { - match expanded_dir_ids.binary_search(&entry_id) { - Ok(ix) => { - expanded_dir_ids.remove(ix); - } - Err(ix) => { - project.expand_entry(worktree_id, entry_id, cx); - expanded_dir_ids.insert(ix, entry_id); - } - } - }); - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - cx.focus(&self.focus_handle); - cx.notify(); - } - } - } - - fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { - if let Some(selection) = self.selection { - let (mut worktree_ix, mut entry_ix, _) = - self.index_for_selection(selection).unwrap_or_default(); - if entry_ix > 0 { - entry_ix -= 1; - } else if worktree_ix > 0 { - worktree_ix -= 1; - entry_ix = self.visible_entries[worktree_ix].1.len() - 1; - } else { - return; - } - - let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix]; - self.selection = Some(Selection { - worktree_id: *worktree_id, - entry_id: worktree_entries[entry_ix].id, - }); - self.autoscroll(cx); - cx.notify(); - } else { - self.select_first(cx); - } - } - - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { - if let Some(task) = self.confirm_edit(cx) { - task.detach_and_log_err(cx); - } - } - - fn open_file(&mut self, _: &Open, cx: &mut ViewContext) { - if let Some((_, entry)) = self.selected_entry(cx) { - if entry.is_file() { - self.open_entry(entry.id, true, cx); - } - } - } - - fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { - let edit_state = self.edit_state.as_mut()?; - cx.focus(&self.focus_handle); - - let worktree_id = edit_state.worktree_id; - let is_new_entry = edit_state.is_new_entry; - let is_dir = edit_state.is_dir; - let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?; - let entry = worktree.read(cx).entry_for_id(edit_state.entry_id)?.clone(); - let filename = self.filename_editor.read(cx).text(cx); - - let path_already_exists = |path| worktree.read(cx).entry_for_path(path).is_some(); - let edit_task; - let edited_entry_id; - if is_new_entry { - self.selection = Some(Selection { - worktree_id, - entry_id: NEW_ENTRY_ID, - }); - let new_path = entry.path.join(&filename.trim_start_matches("/")); - if path_already_exists(new_path.as_path()) { - return None; - } - - edited_entry_id = NEW_ENTRY_ID; - edit_task = self.project.update(cx, |project, cx| { - project.create_entry((worktree_id, &new_path), is_dir, cx) - }); - } else { - let new_path = if let Some(parent) = entry.path.clone().parent() { - parent.join(&filename) - } else { - filename.clone().into() - }; - if path_already_exists(new_path.as_path()) { - return None; - } - - edited_entry_id = entry.id; - edit_task = self.project.update(cx, |project, cx| { - project.rename_entry(entry.id, new_path.as_path(), cx) - }); - }; - - edit_state.processing_filename = Some(filename); - cx.notify(); - - Some(cx.spawn(|this, mut cx| async move { - let new_entry = edit_task.await; - this.update(&mut cx, |this, cx| { - this.edit_state.take(); - cx.notify(); - })?; - - if let Some(new_entry) = new_entry? { - this.update(&mut cx, |this, cx| { - if let Some(selection) = &mut this.selection { - if selection.entry_id == edited_entry_id { - selection.worktree_id = worktree_id; - selection.entry_id = new_entry.id; - this.expand_to_selection(cx); - } - } - this.update_visible_entries(None, cx); - if is_new_entry && !is_dir { - this.open_entry(new_entry.id, true, cx); - } - cx.notify(); - })?; - } - Ok(()) - })) - } - - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - self.edit_state = None; - self.update_visible_entries(None, cx); - cx.focus(&self.focus_handle); - cx.notify(); - } - - fn open_entry( - &mut self, - entry_id: ProjectEntryId, - focus_opened_item: bool, - cx: &mut ViewContext, - ) { - cx.emit(Event::OpenedEntry { - entry_id, - focus_opened_item, - }); - } - - fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { - cx.emit(Event::SplitEntry { entry_id }); - } - - fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext) { - self.add_entry(false, cx) - } - - fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext) { - self.add_entry(true, cx) - } - - fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext) { - if let Some(Selection { - worktree_id, - entry_id, - }) = self.selection - { - let directory_id; - if let Some((worktree, expanded_dir_ids)) = self - .project - .read(cx) - .worktree_for_id(worktree_id, cx) - .zip(self.expanded_dir_ids.get_mut(&worktree_id)) - { - let worktree = worktree.read(cx); - if let Some(mut entry) = worktree.entry_for_id(entry_id) { - loop { - if entry.is_dir() { - if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { - expanded_dir_ids.insert(ix, entry.id); - } - directory_id = entry.id; - break; - } else { - if let Some(parent_path) = entry.path.parent() { - if let Some(parent_entry) = worktree.entry_for_path(parent_path) { - entry = parent_entry; - continue; - } - } - return; - } - } - } else { - return; - }; - } else { - return; - }; - - self.edit_state = Some(EditState { - worktree_id, - entry_id: directory_id, - is_new_entry: true, - is_dir, - processing_filename: None, - }); - self.filename_editor.update(cx, |editor, cx| { - editor.clear(cx); - editor.focus(cx); - }); - self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); - self.autoscroll(cx); - cx.notify(); - } - } - - fn rename(&mut self, _: &Rename, cx: &mut ViewContext) { - if let Some(Selection { - worktree_id, - entry_id, - }) = self.selection - { - if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { - if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { - self.edit_state = Some(EditState { - worktree_id, - entry_id, - is_new_entry: false, - is_dir: entry.is_dir(), - processing_filename: None, - }); - let file_name = entry - .path - .file_name() - .map(|s| s.to_string_lossy()) - .unwrap_or_default() - .to_string(); - let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); - let selection_end = - file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); - self.filename_editor.update(cx, |editor, cx| { - editor.set_text(file_name, cx); - editor.change_selections(Some(Autoscroll::fit()), cx, |s| { - s.select_ranges([0..selection_end]) - }); - editor.focus(cx); - }); - self.update_visible_entries(None, cx); - self.autoscroll(cx); - cx.notify(); - } - } - - // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { - // drag_and_drop.cancel_dragging::(cx); - // }) - } - } - - fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { - maybe!({ - let Selection { entry_id, .. } = self.selection?; - let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; - let file_name = path.file_name()?; - - let answer = cx.prompt( - PromptLevel::Info, - &format!("Delete {file_name:?}?"), - &["Delete", "Cancel"], - ); - - cx.spawn(|this, mut cx| async move { - if answer.await != Ok(0) { - return Ok(()); - } - this.update(&mut cx, |this, cx| { - this.project - .update(cx, |project, cx| project.delete_entry(entry_id, cx)) - .ok_or_else(|| anyhow!("no such entry")) - })?? - .await - }) - .detach_and_log_err(cx); - Some(()) - }); - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(selection) = self.selection { - let (mut worktree_ix, mut entry_ix, _) = - self.index_for_selection(selection).unwrap_or_default(); - if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) { - if entry_ix + 1 < worktree_entries.len() { - entry_ix += 1; - } else { - worktree_ix += 1; - entry_ix = 0; - } - } - - if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) { - if let Some(entry) = worktree_entries.get(entry_ix) { - self.selection = Some(Selection { - worktree_id: *worktree_id, - entry_id: entry.id, - }); - self.autoscroll(cx); - cx.notify(); - } - } - } else { - self.select_first(cx); - } - } - - fn select_first(&mut self, cx: &mut ViewContext) { - let worktree = self - .visible_entries - .first() - .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); - if let Some(worktree) = worktree { - let worktree = worktree.read(cx); - let worktree_id = worktree.id(); - if let Some(root_entry) = worktree.root_entry() { - self.selection = Some(Selection { - worktree_id, - entry_id: root_entry.id, - }); - self.autoscroll(cx); - cx.notify(); - } - } - } - - fn autoscroll(&mut self, cx: &mut ViewContext) { - if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { - self.list.scroll_to_item(index); - cx.notify(); - } - } - - fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { - if let Some((worktree, entry)) = self.selected_entry(cx) { - self.clipboard_entry = Some(ClipboardEntry::Cut { - worktree_id: worktree.id(), - entry_id: entry.id, - }); - cx.notify(); - } - } - - fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { - if let Some((worktree, entry)) = self.selected_entry(cx) { - self.clipboard_entry = Some(ClipboardEntry::Copied { - worktree_id: worktree.id(), - entry_id: entry.id, - }); - cx.notify(); - } - } - - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { - maybe!({ - let (worktree, entry) = self.selected_entry(cx)?; - let clipboard_entry = self.clipboard_entry?; - if clipboard_entry.worktree_id() != worktree.id() { - return None; - } - - let clipboard_entry_file_name = self - .project - .read(cx) - .path_for_entry(clipboard_entry.entry_id(), cx)? - .path - .file_name()? - .to_os_string(); - - let mut new_path = entry.path.to_path_buf(); - if entry.is_file() { - new_path.pop(); - } - - new_path.push(&clipboard_entry_file_name); - let extension = new_path.extension().map(|e| e.to_os_string()); - let file_name_without_extension = Path::new(&clipboard_entry_file_name).file_stem()?; - let mut ix = 0; - while worktree.entry_for_path(&new_path).is_some() { - new_path.pop(); - - let mut new_file_name = file_name_without_extension.to_os_string(); - new_file_name.push(" copy"); - if ix > 0 { - new_file_name.push(format!(" {}", ix)); - } - if let Some(extension) = extension.as_ref() { - new_file_name.push("."); - new_file_name.push(extension); - } - - new_path.push(new_file_name); - ix += 1; - } - - if clipboard_entry.is_cut() { - self.project - .update(cx, |project, cx| { - project.rename_entry(clipboard_entry.entry_id(), new_path, cx) - }) - .detach_and_log_err(cx) - } else { - self.project - .update(cx, |project, cx| { - project.copy_entry(clipboard_entry.entry_id(), new_path, cx) - }) - .detach_and_log_err(cx) - } - - Some(()) - }); - } - - fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { - if let Some((worktree, entry)) = self.selected_entry(cx) { - cx.write_to_clipboard(ClipboardItem::new( - worktree - .abs_path() - .join(&entry.path) - .to_string_lossy() - .to_string(), - )); - } - } - - fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { - if let Some((_, entry)) = self.selected_entry(cx) { - cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string())); - } - } - - fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { - if let Some((worktree, entry)) = self.selected_entry(cx) { - cx.reveal_path(&worktree.abs_path().join(&entry.path)); - } - } - - fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext) { - todo!() - // if let Some((worktree, entry)) = self.selected_entry(cx) { - // let window = cx.window(); - // let view_id = cx.view_id(); - // let path = worktree.abs_path().join(&entry.path); - - // cx.app_context() - // .spawn(|mut cx| async move { - // window.dispatch_action( - // view_id, - // &workspace::OpenTerminal { - // working_directory: path, - // }, - // &mut cx, - // ); - // }) - // .detach(); - // } - } - - pub fn new_search_in_directory( - &mut self, - _: &NewSearchInDirectory, - cx: &mut ViewContext, - ) { - if let Some((_, entry)) = self.selected_entry(cx) { - if entry.is_dir() { - let entry = entry.clone(); - self.workspace - .update(cx, |workspace, cx| { - search::ProjectSearchView::new_search_in_directory(workspace, &entry, cx); - }) - .ok(); - } - } - } - - fn move_entry( - &mut self, - entry_to_move: ProjectEntryId, - destination: ProjectEntryId, - destination_is_file: bool, - cx: &mut ViewContext, - ) { - let destination_worktree = self.project.update(cx, |project, cx| { - let entry_path = project.path_for_entry(entry_to_move, cx)?; - let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); - - let mut destination_path = destination_entry_path.as_ref(); - if destination_is_file { - destination_path = destination_path.parent()?; - } - - let mut new_path = destination_path.to_path_buf(); - new_path.push(entry_path.path.file_name()?); - if new_path != entry_path.path.as_ref() { - let task = project.rename_entry(entry_to_move, new_path, cx); - cx.foreground_executor().spawn(task).detach_and_log_err(cx); - } - - Some(project.worktree_id_for_entry(destination, cx)?) - }); - - if let Some(destination_worktree) = destination_worktree { - self.expand_entry(destination_worktree, destination, cx); - } - } - - fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { - let mut entry_index = 0; - let mut visible_entries_index = 0; - for (worktree_index, (worktree_id, worktree_entries)) in - self.visible_entries.iter().enumerate() - { - if *worktree_id == selection.worktree_id { - for entry in worktree_entries { - if entry.id == selection.entry_id { - return Some((worktree_index, entry_index, visible_entries_index)); - } else { - visible_entries_index += 1; - entry_index += 1; - } - } - break; - } else { - visible_entries_index += worktree_entries.len(); - } - } - None - } - - pub fn selected_entry<'a>( - &self, - cx: &'a AppContext, - ) -> Option<(&'a Worktree, &'a project::Entry)> { - let (worktree, entry) = self.selected_entry_handle(cx)?; - Some((worktree.read(cx), entry)) - } - - fn selected_entry_handle<'a>( - &self, - cx: &'a AppContext, - ) -> Option<(Model, &'a project::Entry)> { - let selection = self.selection?; - let project = self.project.read(cx); - let worktree = project.worktree_for_id(selection.worktree_id, cx)?; - let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; - Some((worktree, entry)) - } - - fn expand_to_selection(&mut self, cx: &mut ViewContext) -> Option<()> { - let (worktree, entry) = self.selected_entry(cx)?; - let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default(); - - for path in entry.path.ancestors() { - let Some(entry) = worktree.entry_for_path(path) else { - continue; - }; - if entry.is_dir() { - if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { - expanded_dir_ids.insert(idx, entry.id); - } - } - } - - Some(()) - } - - fn update_visible_entries( - &mut self, - new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, - cx: &mut ViewContext, - ) { - let project = self.project.read(cx); - self.last_worktree_root_id = project - .visible_worktrees(cx) - .rev() - .next() - .and_then(|worktree| worktree.read(cx).root_entry()) - .map(|entry| entry.id); - - self.visible_entries.clear(); - for worktree in project.visible_worktrees(cx) { - let snapshot = worktree.read(cx).snapshot(); - let worktree_id = snapshot.id(); - - let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) { - hash_map::Entry::Occupied(e) => e.into_mut(), - hash_map::Entry::Vacant(e) => { - // The first time a worktree's root entry becomes available, - // mark that root entry as expanded. - if let Some(entry) = snapshot.root_entry() { - e.insert(vec![entry.id]).as_slice() - } else { - &[] - } - } - }; - - let mut new_entry_parent_id = None; - let mut new_entry_kind = EntryKind::Dir; - if let Some(edit_state) = &self.edit_state { - if edit_state.worktree_id == worktree_id && edit_state.is_new_entry { - new_entry_parent_id = Some(edit_state.entry_id); - new_entry_kind = if edit_state.is_dir { - EntryKind::Dir - } else { - EntryKind::File(Default::default()) - }; - } - } - - let mut visible_worktree_entries = Vec::new(); - let mut entry_iter = snapshot.entries(true); - - while let Some(entry) = entry_iter.entry() { - visible_worktree_entries.push(entry.clone()); - if Some(entry.id) == new_entry_parent_id { - visible_worktree_entries.push(Entry { - id: NEW_ENTRY_ID, - kind: new_entry_kind, - path: entry.path.join("\0").into(), - inode: 0, - mtime: entry.mtime, - is_symlink: false, - is_ignored: false, - is_external: false, - git_status: entry.git_status, - }); - } - if expanded_dir_ids.binary_search(&entry.id).is_err() - && entry_iter.advance_to_sibling() - { - continue; - } - entry_iter.advance(); - } - - snapshot.propagate_git_statuses(&mut visible_worktree_entries); - - visible_worktree_entries.sort_by(|entry_a, entry_b| { - let mut components_a = entry_a.path.components().peekable(); - let mut components_b = entry_b.path.components().peekable(); - loop { - match (components_a.next(), components_b.next()) { - (Some(component_a), Some(component_b)) => { - let a_is_file = components_a.peek().is_none() && entry_a.is_file(); - let b_is_file = components_b.peek().is_none() && entry_b.is_file(); - let ordering = a_is_file.cmp(&b_is_file).then_with(|| { - let name_a = - UniCase::new(component_a.as_os_str().to_string_lossy()); - let name_b = - UniCase::new(component_b.as_os_str().to_string_lossy()); - name_a.cmp(&name_b) - }); - if !ordering.is_eq() { - return ordering; - } - } - (Some(_), None) => break Ordering::Greater, - (None, Some(_)) => break Ordering::Less, - (None, None) => break Ordering::Equal, - } - } - }); - self.visible_entries - .push((worktree_id, visible_worktree_entries)); - } - - if let Some((worktree_id, entry_id)) = new_selected_entry { - self.selection = Some(Selection { - worktree_id, - entry_id, - }); - } - } - - fn expand_entry( - &mut self, - worktree_id: WorktreeId, - entry_id: ProjectEntryId, - cx: &mut ViewContext, - ) { - self.project.update(cx, |project, cx| { - if let Some((worktree, expanded_dir_ids)) = project - .worktree_for_id(worktree_id, cx) - .zip(self.expanded_dir_ids.get_mut(&worktree_id)) - { - project.expand_entry(worktree_id, entry_id, cx); - let worktree = worktree.read(cx); - - if let Some(mut entry) = worktree.entry_for_id(entry_id) { - loop { - if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { - expanded_dir_ids.insert(ix, entry.id); - } - - if let Some(parent_entry) = - entry.path.parent().and_then(|p| worktree.entry_for_path(p)) - { - entry = parent_entry; - } else { - break; - } - } - } - } - }); - } - - fn for_each_visible_entry( - &self, - range: Range, - cx: &mut ViewContext, - mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), - ) { - let mut ix = 0; - for (worktree_id, visible_worktree_entries) in &self.visible_entries { - if ix >= range.end { - return; - } - - if ix + visible_worktree_entries.len() <= range.start { - ix += visible_worktree_entries.len(); - continue; - } - - let end_ix = range.end.min(ix + visible_worktree_entries.len()); - let (git_status_setting, show_file_icons, show_folder_icons) = { - let settings = ProjectPanelSettings::get_global(cx); - ( - settings.git_status, - settings.file_icons, - settings.folder_icons, - ) - }; - if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { - let snapshot = worktree.read(cx).snapshot(); - let root_name = OsStr::new(snapshot.root_name()); - let expanded_entry_ids = self - .expanded_dir_ids - .get(&snapshot.id()) - .map(Vec::as_slice) - .unwrap_or(&[]); - - let entry_range = range.start.saturating_sub(ix)..end_ix - ix; - for entry in visible_worktree_entries[entry_range].iter() { - let status = git_status_setting.then(|| entry.git_status).flatten(); - let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); - let icon = match entry.kind { - EntryKind::File(_) => { - if show_file_icons { - FileAssociations::get_icon(&entry.path, cx) - } else { - None - } - } - _ => { - if show_folder_icons { - FileAssociations::get_folder_icon(is_expanded, cx) - } else { - FileAssociations::get_chevron_icon(is_expanded, cx) - } - } - }; - - let mut details = EntryDetails { - filename: entry - .path - .file_name() - .unwrap_or(root_name) - .to_string_lossy() - .to_string(), - icon, - path: entry.path.clone(), - depth: entry.path.components().count(), - kind: entry.kind, - is_ignored: entry.is_ignored, - is_expanded, - is_selected: self.selection.map_or(false, |e| { - e.worktree_id == snapshot.id() && e.entry_id == entry.id - }), - is_editing: false, - is_processing: false, - is_cut: self - .clipboard_entry - .map_or(false, |e| e.is_cut() && e.entry_id() == entry.id), - git_status: status, - }; - - if let Some(edit_state) = &self.edit_state { - let is_edited_entry = if edit_state.is_new_entry { - entry.id == NEW_ENTRY_ID - } else { - entry.id == edit_state.entry_id - }; - - if is_edited_entry { - if let Some(processing_filename) = &edit_state.processing_filename { - details.is_processing = true; - details.filename.clear(); - details.filename.push_str(processing_filename); - } else { - if edit_state.is_new_entry { - details.filename.clear(); - } - details.is_editing = true; - } - } - } - - callback(entry.id, details, cx); - } - } - ix = end_ix; - } - } - - fn render_entry( - &self, - entry_id: ProjectEntryId, - details: EntryDetails, - cx: &mut ViewContext, - ) -> Stateful
{ - let kind = details.kind; - let settings = ProjectPanelSettings::get_global(cx); - let show_editor = details.is_editing && !details.is_processing; - let is_selected = self - .selection - .map_or(false, |selection| selection.entry_id == entry_id); - let width = self.width.unwrap_or(px(0.)); - - let filename_text_color = details - .git_status - .as_ref() - .map(|status| match status { - GitFileStatus::Added => Color::Created, - GitFileStatus::Modified => Color::Modified, - GitFileStatus::Conflict => Color::Conflict, - }) - .unwrap_or(if is_selected { - Color::Default - } else { - Color::Muted - }); - - let file_name = details.filename.clone(); - let icon = details.icon.clone(); - let depth = details.depth; - div() - .id(entry_id.to_proto() as usize) - .on_drag(entry_id, move |entry_id, cx| { - cx.new_view(|_| DraggedProjectEntryView { - details: details.clone(), - width, - entry_id: *entry_id, - }) - }) - .drag_over::(|style| { - style.bg(cx.theme().colors().drop_target_background) - }) - .on_drop(cx.listener(move |this, dragged_id: &ProjectEntryId, cx| { - this.move_entry(*dragged_id, entry_id, kind.is_file(), cx); - })) - .child( - ListItem::new(entry_id.to_proto() as usize) - .indent_level(depth) - .indent_step_size(px(settings.indent_size)) - .selected(is_selected) - .child(if let Some(icon) = &icon { - div().child(IconElement::from_path(icon.to_string()).color(Color::Muted)) - } else { - div() - }) - .child( - if let (Some(editor), true) = (Some(&self.filename_editor), show_editor) { - div().h_full().w_full().child(editor.clone()) - } else { - div().child(Label::new(file_name).color(filename_text_color)) - } - .ml_1(), - ) - .on_click(cx.listener(move |this, event: &gpui::ClickEvent, cx| { - if event.down.button == MouseButton::Right { - return; - } - if !show_editor { - if kind.is_dir() { - this.toggle_expanded(entry_id, cx); - } else { - if event.down.modifiers.command { - this.split_entry(entry_id, cx); - } else { - this.open_entry(entry_id, event.up.click_count > 1, cx); - } - } - } - })) - .on_secondary_mouse_down(cx.listener( - move |this, event: &MouseDownEvent, cx| { - this.deploy_context_menu(event.position, entry_id, cx); - }, - )), - ) - } - - fn dispatch_context(&self, cx: &ViewContext) -> KeyContext { - let mut dispatch_context = KeyContext::default(); - dispatch_context.add("ProjectPanel"); - dispatch_context.add("menu"); - - let identifier = if self.filename_editor.focus_handle(cx).is_focused(cx) { - "editing" - } else { - "not_editing" - }; - - dispatch_context.add(identifier); - dispatch_context - } - - fn reveal_entry( - &mut self, - project: Model, - entry_id: ProjectEntryId, - skip_ignored: bool, - cx: &mut ViewContext<'_, ProjectPanel>, - ) { - if let Some(worktree) = project.read(cx).worktree_for_entry(entry_id, cx) { - let worktree = worktree.read(cx); - if skip_ignored - && worktree - .entry_for_id(entry_id) - .map_or(true, |entry| entry.is_ignored) - { - return; - } - - let worktree_id = worktree.id(); - self.expand_entry(worktree_id, entry_id, cx); - self.update_visible_entries(Some((worktree_id, entry_id)), cx); - self.autoscroll(cx); - cx.notify(); - } - } -} - -impl Render for ProjectPanel { - fn render(&mut self, cx: &mut gpui::ViewContext) -> impl IntoElement { - let has_worktree = self.visible_entries.len() != 0; - - if has_worktree { - div() - .id("project-panel") - .size_full() - .relative() - .key_context(self.dispatch_context(cx)) - .on_action(cx.listener(Self::select_next)) - .on_action(cx.listener(Self::select_prev)) - .on_action(cx.listener(Self::expand_selected_entry)) - .on_action(cx.listener(Self::collapse_selected_entry)) - .on_action(cx.listener(Self::collapse_all_entries)) - .on_action(cx.listener(Self::new_file)) - .on_action(cx.listener(Self::new_directory)) - .on_action(cx.listener(Self::rename)) - .on_action(cx.listener(Self::delete)) - .on_action(cx.listener(Self::confirm)) - .on_action(cx.listener(Self::open_file)) - .on_action(cx.listener(Self::cancel)) - .on_action(cx.listener(Self::cut)) - .on_action(cx.listener(Self::copy)) - .on_action(cx.listener(Self::copy_path)) - .on_action(cx.listener(Self::copy_relative_path)) - .on_action(cx.listener(Self::paste)) - .on_action(cx.listener(Self::reveal_in_finder)) - .on_action(cx.listener(Self::open_in_terminal)) - .on_action(cx.listener(Self::new_search_in_directory)) - .track_focus(&self.focus_handle) - .child( - uniform_list( - cx.view().clone(), - "entries", - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - { - |this, range, cx| { - let mut items = Vec::new(); - this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(this.render_entry(id, details, cx)); - }); - items - } - }, - ) - .size_full() - .track_scroll(self.list.clone()), - ) - .children(self.context_menu.as_ref().map(|(menu, position, _)| { - overlay() - .position(*position) - .anchor(gpui::AnchorCorner::TopLeft) - .child(menu.clone()) - })) - } else { - v_stack() - .id("empty-project_panel") - .track_focus(&self.focus_handle) - } - } -} - -impl Render for DraggedProjectEntryView { - fn render(&mut self, cx: &mut ViewContext) -> impl Element { - let settings = ProjectPanelSettings::get_global(cx); - let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - h_stack() - .font(ui_font) - .bg(cx.theme().colors().background) - .w(self.width) - .child( - ListItem::new(self.entry_id.to_proto() as usize) - .indent_level(self.details.depth) - .indent_step_size(px(settings.indent_size)) - .child(if let Some(icon) = &self.details.icon { - div().child(IconElement::from_path(icon.to_string())) - } else { - div() - }) - .child(Label::new(self.details.filename.clone())), - ) - } -} - -impl EventEmitter for ProjectPanel {} - -impl EventEmitter for ProjectPanel {} - -impl Panel for ProjectPanel { - fn position(&self, cx: &WindowContext) -> DockPosition { - match ProjectPanelSettings::get_global(cx).dock { - ProjectPanelDockPosition::Left => DockPosition::Left, - ProjectPanelDockPosition::Right => DockPosition::Right, - } - } - - fn position_is_valid(&self, position: DockPosition) -> bool { - matches!(position, DockPosition::Left | DockPosition::Right) - } - - fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { - settings::update_settings_file::( - self.fs.clone(), - cx, - move |settings| { - let dock = match position { - DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, - DockPosition::Right => ProjectPanelDockPosition::Right, - }; - settings.dock = Some(dock); - }, - ); - } - - fn size(&self, cx: &WindowContext) -> Pixels { - self.width - .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) - } - - fn set_size(&mut self, size: Option, cx: &mut ViewContext) { - self.width = size; - self.serialize(cx); - cx.notify(); - } - - fn icon(&self, _: &WindowContext) -> Option { - Some(ui::Icon::FileTree) - } - - fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> { - Some("Project Panel") - } - - fn toggle_action(&self) -> Box { - Box::new(ToggleFocus) - } - - fn persistent_name() -> &'static str { - "Project Panel" - } -} - -impl FocusableView for ProjectPanel { - fn focus_handle(&self, _cx: &AppContext) -> FocusHandle { - self.focus_handle.clone() - } -} - -impl ClipboardEntry { - fn is_cut(&self) -> bool { - matches!(self, Self::Cut { .. }) - } - - fn entry_id(&self) -> ProjectEntryId { - match self { - ClipboardEntry::Copied { entry_id, .. } | ClipboardEntry::Cut { entry_id, .. } => { - *entry_id - } - } - } - - fn worktree_id(&self) -> WorktreeId { - match self { - ClipboardEntry::Copied { worktree_id, .. } - | ClipboardEntry::Cut { worktree_id, .. } => *worktree_id, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use gpui::{TestAppContext, View, VisualTestContext, WindowHandle}; - use pretty_assertions::assert_eq; - use project::{project_settings::ProjectSettings, FakeFs}; - use serde_json::json; - use settings::SettingsStore; - use std::{ - collections::HashSet, - path::{Path, PathBuf}, - }; - use workspace::AppState; - - #[gpui::test] - async fn test_visible_list(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root1/b", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > .git", - " > a", - " v b <== selected", - " > 3", - " > 4", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - assert_eq!( - visible_entries_as_strings(&panel, 6..9, cx), - &[ - // - " > C", - " .dockerignore", - "v root2", - ] - ); - } - - #[gpui::test] - async fn test_exclusions_in_visible_list(cx: &mut gpui::TestAppContext) { - init_test(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = - Some(vec!["**/.git".to_string(), "**/4/**".to_string()]); - }); - }); - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "4": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root1/b", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b <== selected", - " > 3", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root2/d", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b", - " > 3", - " > C", - " .dockerignore", - "v root2", - " v d <== selected", - " > e", - ] - ); - - toggle_expand_dir(&panel, "root2/e", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - "v root1", - " > a", - " v b", - " > 3", - " > C", - " .dockerignore", - "v root2", - " v d", - " v e <== selected", - ] - ); - } - - #[gpui::test(iterations = 30)] - async fn test_editing_files(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| { - let panel = ProjectPanel::new(workspace, cx); - workspace.add_panel(panel.clone(), cx); - workspace.toggle_dock(panel.read(cx).position(cx), cx); - panel - }) - .unwrap(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1 <== selected", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - panel.update(cx, |panel, cx| { - assert!(panel.filename_editor.read(cx).is_focused(cx)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [EDITOR: ''] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - let confirm = panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("the-new-filename", cx)); - panel.confirm_edit(cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [PROCESSING: 'the-new-filename'] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - confirm.await.unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - " the-new-filename <== selected", - "v root2", - " > d", - " > e", - ] - ); - - select_path(&panel, "root1/b", cx); - panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: ''] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel - .update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("another-filename.txt", cx)); - panel.confirm_edit(cx).unwrap() - }) - .await - .unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " another-filename.txt <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - select_path(&panel, "root1/b/another-filename.txt", cx); - panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: 'another-filename.txt'] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - let confirm = panel.update(cx, |panel, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(cx); - assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); - let file_name_selection = &file_name_selections[0]; - assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); - assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); - - editor.set_text("a-different-filename.tar.gz", cx) - }); - panel.confirm_edit(cx).unwrap() - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - confirm.await.unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " a-different-filename.tar.gz <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3", - " > 4", - " [EDITOR: 'a-different-filename.tar.gz'] <== selected", - " > C", - " .dockerignore", - " the-new-filename", - ] - ); - - panel.update(cx, |panel, cx| { - panel.filename_editor.update(cx, |editor, cx| { - let file_name_selections = editor.selections.all::(cx); - assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); - let file_name_selection = &file_name_selections[0]; - assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); - assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot.."); - - }); - panel.cancel(&Cancel, cx) - }); - - panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [EDITOR: ''] <== selected", - " > 3", - " > 4", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - let confirm = panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("new-dir", cx)); - panel.confirm_edit(cx).unwrap() - }); - panel.update(cx, |panel, cx| panel.select_next(&Default::default(), cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [PROCESSING: 'new-dir']", - " > 3 <== selected", - " > 4", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - confirm.await.unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3 <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - panel.update(cx, |panel, cx| panel.rename(&Default::default(), cx)); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > [EDITOR: '3'] <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - - // Dismiss the rename editor when it loses focus. - workspace.update(cx, |_, cx| cx.blur()).unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " v b", - " > 3 <== selected", - " > 4", - " > new-dir", - " a-different-filename.tar.gz", - " > C", - " .dockerignore", - ] - ); - } - - #[gpui::test(iterations = 10)] - async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/root1", - json!({ - ".dockerignore": "", - ".git": { - "HEAD": "", - }, - "a": { - "0": { "q": "", "r": "", "s": "" }, - "1": { "t": "", "u": "" }, - "2": { "v": "", "w": "", "x": "", "y": "" }, - }, - "b": { - "3": { "Q": "" }, - "4": { "R": "", "S": "", "T": "", "U": "" }, - }, - "C": { - "5": {}, - "6": { "V": "", "W": "" }, - "7": { "X": "" }, - "8": { "Y": {}, "Z": "" } - } - }), - ) - .await; - fs.insert_tree( - "/root2", - json!({ - "d": { - "9": "" - }, - "e": {} - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| { - let panel = ProjectPanel::new(workspace, cx); - workspace.add_panel(panel.clone(), cx); - workspace.toggle_dock(panel.read(cx).position(cx), cx); - panel - }) - .unwrap(); - - select_path(&panel, "root1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1 <== selected", - " > .git", - " > a", - " > b", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - // Add a file with the root folder selected. The filename editor is placed - // before the first file in the root folder. - panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - panel.update(cx, |panel, cx| { - assert!(panel.filename_editor.read(cx).is_focused(cx)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [EDITOR: ''] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - let confirm = panel.update(cx, |panel, cx| { - panel.filename_editor.update(cx, |editor, cx| { - editor.set_text("/bdir1/dir2/the-new-filename", cx) - }); - panel.confirm_edit(cx).unwrap() - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " > C", - " [PROCESSING: '/bdir1/dir2/the-new-filename'] <== selected", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - - confirm.await.unwrap(); - assert_eq!( - visible_entries_as_strings(&panel, 0..13, cx), - &[ - "v root1", - " > .git", - " > a", - " > b", - " v bdir1", - " v dir2", - " the-new-filename <== selected", - " > C", - " .dockerignore", - "v root2", - " > d", - " > e", - ] - ); - } - - #[gpui::test] - async fn test_copy_paste(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/root1", - json!({ - "one.two.txt": "", - "one.txt": "" - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/root1".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - - panel.update(cx, |panel, cx| { - panel.select_next(&Default::default(), cx); - panel.select_next(&Default::default(), cx); - }); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.two.txt <== selected", - " one.txt", - ] - ); - - // Regression test - file name is created correctly when - // the copied file's name contains multiple dots. - panel.update(cx, |panel, cx| { - panel.copy(&Default::default(), cx); - panel.paste(&Default::default(), cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.two copy.txt", - " one.two.txt <== selected", - " one.txt", - ] - ); - - panel.update(cx, |panel, cx| { - panel.paste(&Default::default(), cx); - }); - cx.executor().run_until_parked(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..50, cx), - &[ - // - "v root1", - " one.two copy 1.txt", - " one.two copy.txt", - " one.two.txt <== selected", - " one.txt", - ] - ); - } - - #[gpui::test] - async fn test_remove_opened_file(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - - toggle_expand_dir(&panel, "src/test", cx); - select_path(&panel, "src/test/first.rs", cx); - panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/first.rs", cx); - - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " second.rs", - " third.rs" - ], - "Project panel should have no deleted file, no other file is selected in it" - ); - ensure_no_open_items_and_panes(&workspace, cx); - - select_path(&panel, "src/test/second.rs", cx); - panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " second.rs <== selected", - " third.rs" - ] - ); - ensure_single_file_is_opened(&workspace, "test/second.rs", cx); - - workspace - .update(cx, |workspace, cx| { - let active_items = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()) - .collect::>(); - assert_eq!(active_items.len(), 1); - let open_editor = active_items - .into_iter() - .next() - .unwrap() - .downcast::() - .expect("Open item should be an editor"); - open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); - }) - .unwrap(); - submit_deletion(&panel, cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v src", " v test", " third.rs"], - "Project panel should have no deleted file, with one last file remaining" - ); - ensure_no_open_items_and_panes(&workspace, cx); - } - - #[gpui::test] - async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/src", - json!({ - "test": { - "first.rs": "// First Rust file", - "second.rs": "// Second Rust file", - "third.rs": "// Third Rust file", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| { - let panel = ProjectPanel::new(workspace, cx); - workspace.add_panel(panel.clone(), cx); - workspace.toggle_dock(panel.read(cx).position(cx), cx); - panel - }) - .unwrap(); - - select_path(&panel, "src/", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src <== selected", - " > test" - ] - ); - panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); - panel.update(cx, |panel, cx| { - assert!(panel.filename_editor.read(cx).is_focused(cx)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > [EDITOR: ''] <== selected", - " > test" - ] - ); - panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("test", cx)); - assert!( - panel.confirm_edit(cx).is_none(), - "Should not allow to confirm on conflicting new directory name" - ) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test" - ], - "File list should be unchanged after failed folder create confirmation" - ); - - select_path(&panel, "src/test/", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - // - "v src", - " > test <== selected" - ] - ); - panel.update(cx, |panel, cx| panel.new_file(&NewFile, cx)); - panel.update(cx, |panel, cx| { - assert!(panel.filename_editor.read(cx).is_focused(cx)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: ''] <== selected", - " first.rs", - " second.rs", - " third.rs" - ] - ); - panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("first.rs", cx)); - assert!( - panel.confirm_edit(cx).is_none(), - "Should not allow to confirm on conflicting new file name" - ) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs", - " second.rs", - " third.rs" - ], - "File list should be unchanged after failed file create confirmation" - ); - - select_path(&panel, "src/test/first.rs", cx); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ], - ); - panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); - panel.update(cx, |panel, cx| { - assert!(panel.filename_editor.read(cx).is_focused(cx)); - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " [EDITOR: 'first.rs'] <== selected", - " second.rs", - " third.rs" - ] - ); - panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("second.rs", cx)); - assert!( - panel.confirm_edit(cx).is_none(), - "Should not allow to confirm on conflicting file rename" - ) - }); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v src", - " v test", - " first.rs <== selected", - " second.rs", - " third.rs" - ], - "File list should be unchanged after failed rename confirmation" - ); - } - - #[gpui::test] - async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.insert_tree( - "/project_root", - json!({ - "dir_1": { - "nested_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - }, - "dir_2": { - "file_1.py": "# File contents", - "file_2.py": "# File contents", - "file_3.py": "# File contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - - panel.update(cx, |panel, cx| { - panel.collapse_all_entries(&CollapseAllEntries, cx) - }); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v project_root", " > dir_1", " > dir_2",] - ); - - // Open dir_1 and make sure nested_dir was collapsed when running collapse_all_entries - toggle_expand_dir(&panel, "project_root/dir_1", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &[ - "v project_root", - " v dir_1 <== selected", - " > nested_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - ] - ); - } - - #[gpui::test] - async fn test_new_file_move(cx: &mut gpui::TestAppContext) { - init_test(cx); - - let fs = FakeFs::new(cx.executor().clone()); - fs.as_fake().insert_tree("/root", json!({})).await; - let project = Project::test(fs, ["/root".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - - // Make a new buffer with no backing file - workspace - .update(cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .unwrap(); - - // "Save as"" the buffer, creating a new backing file for it - let save_task = workspace - .update(cx, |workspace, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, cx) - }) - .unwrap(); - - cx.executor().run_until_parked(); - cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); - save_task.await.unwrap(); - - // Rename the file - select_path(&panel, "root/new", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " new <== selected"] - ); - panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); - panel.update(cx, |panel, cx| { - panel - .filename_editor - .update(cx, |editor, cx| editor.set_text("newer", cx)); - }); - panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); - - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " newer <== selected"] - ); - - workspace - .update(cx, |workspace, cx| { - workspace.save_active_item(workspace::SaveIntent::Save, cx) - }) - .unwrap() - .await - .unwrap(); - - cx.executor().run_until_parked(); - // assert that saving the file doesn't restore "new" - assert_eq!( - visible_entries_as_strings(&panel, 0..10, cx), - &["v root", " newer <== selected"] - ); - } - - #[gpui::test] - async fn test_autoreveal_and_gitignored_files(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(Vec::new()); - }); - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(false) - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx) - .expect("dir 1 file is not ignored and should have an entry"); - let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx) - .expect("dir 2 file is not ignored and should have an entry"); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry before its dir is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.executor().run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir <== selected", - " file_a.py", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "Should show gitignored dir file list in the project panel" - ); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx) - .expect("after gitignored dir got opened, a file entry should be present"); - - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - toggle_expand_dir(&panel, "project_root/dir_1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "Should hide all dir contents again and prepare for the auto reveal test" - ); - - for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] { - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "When no auto reveal is enabled, the selected entry should not be revealed in the project panel" - ); - } - - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(true) - }); - }) - }); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(dir_1_file))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py <== selected", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "When auto reveal is enabled, not ignored dir_1 entry should be revealed" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(dir_2_file))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When auto reveal is enabled, not ignored dir_2 entry should be revealed" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some( - gitignored_dir_file, - ))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When auto reveal is enabled, a gitignored selected entry should not be revealed in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir", - " file_a.py <== selected", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "When a gitignored entry is explicitly revealed, it should be shown in the project tree" - ); - } - - #[gpui::test] - async fn test_explicit_reveal(cx: &mut gpui::TestAppContext) { - init_test_with_editor(cx); - cx.update(|cx| { - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(Vec::new()); - }); - store.update_user_settings::(cx, |project_panel_settings| { - project_panel_settings.auto_reveal_entries = Some(false) - }); - }) - }); - - let fs = FakeFs::new(cx.background_executor.clone()); - fs.insert_tree( - "/project_root", - json!({ - ".git": {}, - ".gitignore": "**/gitignored_dir", - "dir_1": { - "file_1.py": "# File 1_1 contents", - "file_2.py": "# File 1_2 contents", - "file_3.py": "# File 1_3 contents", - "gitignored_dir": { - "file_a.py": "# File contents", - "file_b.py": "# File contents", - "file_c.py": "# File contents", - }, - }, - "dir_2": { - "file_1.py": "# File 2_1 contents", - "file_2.py": "# File 2_2 contents", - "file_3.py": "# File 2_3 contents", - } - }), - ) - .await; - - let project = Project::test(fs.clone(), ["/project_root".as_ref()], cx).await; - let workspace = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); - let cx = &mut VisualTestContext::from_window(*workspace, cx); - let panel = workspace - .update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)) - .unwrap(); - - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1", - " > dir_2", - " .gitignore", - ] - ); - - let dir_1_file = find_project_entry(&panel, "project_root/dir_1/file_1.py", cx) - .expect("dir 1 file is not ignored and should have an entry"); - let dir_2_file = find_project_entry(&panel, "project_root/dir_2/file_1.py", cx) - .expect("dir 2 file is not ignored and should have an entry"); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx); - assert_eq!( - gitignored_dir_file, None, - "File in the gitignored dir should not have an entry before its dir is toggled" - ); - - toggle_expand_dir(&panel, "project_root/dir_1", cx); - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir <== selected", - " file_a.py", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "Should show gitignored dir file list in the project panel" - ); - let gitignored_dir_file = - find_project_entry(&panel, "project_root/dir_1/gitignored_dir/file_a.py", cx) - .expect("after gitignored dir got opened, a file entry should be present"); - - toggle_expand_dir(&panel, "project_root/dir_1/gitignored_dir", cx); - toggle_expand_dir(&panel, "project_root/dir_1", cx); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "Should hide all dir contents again and prepare for the explicit reveal test" - ); - - for file_entry in [dir_1_file, dir_2_file, gitignored_dir_file] { - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::ActiveEntryChanged(Some(file_entry))) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " > dir_1 <== selected", - " > dir_2", - " .gitignore", - ], - "When no auto reveal is enabled, the selected entry should not be revealed in the project panel" - ); - } - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(dir_1_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py <== selected", - " file_2.py", - " file_3.py", - " > dir_2", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the dir_1 entry in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(dir_2_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " > gitignored_dir", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py <== selected", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the dir_2 entry in the project panel" - ); - - panel.update(cx, |panel, cx| { - panel.project.update(cx, |_, cx| { - cx.emit(project::Event::RevealInProjectPanel(gitignored_dir_file)) - }) - }); - cx.run_until_parked(); - assert_eq!( - visible_entries_as_strings(&panel, 0..20, cx), - &[ - "v project_root", - " > .git", - " v dir_1", - " v gitignored_dir", - " file_a.py <== selected", - " file_b.py", - " file_c.py", - " file_1.py", - " file_2.py", - " file_3.py", - " v dir_2", - " file_1.py", - " file_2.py", - " file_3.py", - " .gitignore", - ], - "With no auto reveal, explicit reveal should show the gitignored entry in the project panel" - ); - } - - fn toggle_expand_dir( - panel: &View, - path: impl AsRef, - cx: &mut VisualTestContext, - ) { - let path = path.as_ref(); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees().collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.toggle_expanded(entry_id, cx); - return; - } - } - panic!("no worktree for path {:?}", path); - }); - } - - fn select_path(panel: &View, path: impl AsRef, cx: &mut VisualTestContext) { - let path = path.as_ref(); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees().collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - let entry_id = worktree.entry_for_path(relative_path).unwrap().id; - panel.selection = Some(crate::Selection { - worktree_id: worktree.id(), - entry_id, - }); - return; - } - } - panic!("no worktree for path {:?}", path); - }); - } - - fn find_project_entry( - panel: &View, - path: impl AsRef, - cx: &mut VisualTestContext, - ) -> Option { - let path = path.as_ref(); - panel.update(cx, |panel, cx| { - for worktree in panel.project.read(cx).worktrees().collect::>() { - let worktree = worktree.read(cx); - if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { - return worktree.entry_for_path(relative_path).map(|entry| entry.id); - } - } - panic!("no worktree for path {path:?}"); - }) - } - - fn visible_entries_as_strings( - panel: &View, - range: Range, - cx: &mut VisualTestContext, - ) -> Vec { - let mut result = Vec::new(); - let mut project_entries = HashSet::new(); - let mut has_editor = false; - - panel.update(cx, |panel, cx| { - panel.for_each_visible_entry(range, cx, |project_entry, details, _| { - if details.is_editing { - assert!(!has_editor, "duplicate editor entry"); - has_editor = true; - } else { - assert!( - project_entries.insert(project_entry), - "duplicate project entry {:?} {:?}", - project_entry, - details - ); - } - - let indent = " ".repeat(details.depth); - let icon = if details.kind.is_dir() { - if details.is_expanded { - "v " - } else { - "> " - } - } else { - " " - }; - let name = if details.is_editing { - format!("[EDITOR: '{}']", details.filename) - } else if details.is_processing { - format!("[PROCESSING: '{}']", details.filename) - } else { - details.filename.clone() - }; - let selected = if details.is_selected { - " <== selected" - } else { - "" - }; - result.push(format!("{indent}{icon}{name}{selected}")); - }); - }); - - result - } - - fn init_test(cx: &mut TestAppContext) { - cx.update(|cx| { - let settings_store = SettingsStore::test(cx); - cx.set_global(settings_store); - init_settings(cx); - theme::init(theme::LoadThemes::JustBase, cx); - language::init(cx); - editor::init_settings(cx); - crate::init((), cx); - workspace::init_settings(cx); - client::init_settings(cx); - Project::init_settings(cx); - - cx.update_global::(|store, cx| { - store.update_user_settings::(cx, |project_settings| { - project_settings.file_scan_exclusions = Some(Vec::new()); - }); - }); - }); - } - - fn init_test_with_editor(cx: &mut TestAppContext) { - cx.update(|cx| { - let app_state = AppState::test(cx); - theme::init(theme::LoadThemes::JustBase, cx); - init_settings(cx); - language::init(cx); - editor::init(cx); - crate::init((), cx); - workspace::init(app_state.clone(), cx); - Project::init_settings(cx); - }); - } - - fn ensure_single_file_is_opened( - window: &WindowHandle, - expected_path: &str, - cx: &mut TestAppContext, - ) { - window - .update(cx, |workspace, cx| { - let worktrees = workspace.worktrees(cx).collect::>(); - assert_eq!(worktrees.len(), 1); - let worktree_id = worktrees[0].read(cx).id(); - - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert_eq!( - open_project_paths, - vec![ProjectPath { - worktree_id, - path: Arc::from(Path::new(expected_path)) - }], - "Should have opened file, selected in project panel" - ); - }) - .unwrap(); - } - - fn submit_deletion(panel: &View, cx: &mut VisualTestContext) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts before the deletion" - ); - panel.update(cx, |panel, cx| panel.delete(&Delete, cx)); - assert!( - cx.has_pending_prompt(), - "Should have a prompt after the deletion" - ); - cx.simulate_prompt_answer(0); - assert!( - !cx.has_pending_prompt(), - "Should have no prompts after prompt was replied to" - ); - cx.executor().run_until_parked(); - } - - fn ensure_no_open_items_and_panes( - workspace: &WindowHandle, - cx: &mut VisualTestContext, - ) { - assert!( - !cx.has_pending_prompt(), - "Should have no prompts after deletion operation closes the file" - ); - workspace - .read_with(cx, |workspace, cx| { - let open_project_paths = workspace - .panes() - .iter() - .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) - .collect::>(); - assert!( - open_project_paths.is_empty(), - "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" - ); - }) - .unwrap(); - } -} diff --git a/crates/project_panel2/src/project_panel_settings.rs b/crates/project_panel2/src/project_panel_settings.rs deleted file mode 100644 index b9a87a1a03..0000000000 --- a/crates/project_panel2/src/project_panel_settings.rs +++ /dev/null @@ -1,48 +0,0 @@ -use anyhow; -use gpui::Pixels; -use schemars::JsonSchema; -use serde_derive::{Deserialize, Serialize}; -use settings::Settings; - -#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum ProjectPanelDockPosition { - Left, - Right, -} - -#[derive(Deserialize, Debug)] -pub struct ProjectPanelSettings { - pub default_width: Pixels, - pub dock: ProjectPanelDockPosition, - pub file_icons: bool, - pub folder_icons: bool, - pub git_status: bool, - pub indent_size: f32, - pub auto_reveal_entries: bool, -} - -#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] -pub struct ProjectPanelSettingsContent { - pub default_width: Option, - pub dock: Option, - pub file_icons: Option, - pub folder_icons: Option, - pub git_status: Option, - pub indent_size: Option, - pub auto_reveal_entries: Option, -} - -impl Settings for ProjectPanelSettings { - const KEY: Option<&'static str> = Some("project_panel"); - - type FileContent = ProjectPanelSettingsContent; - - fn load( - default_value: &Self::FileContent, - user_values: &[&Self::FileContent], - _: &mut gpui::AppContext, - ) -> anyhow::Result { - Self::load_via_json_merge(default_value, user_values) - } -} diff --git a/crates/recent_projects/Cargo.toml b/crates/recent_projects/Cargo.toml index 51774e8feb..559fa83808 100644 --- a/crates/recent_projects/Cargo.toml +++ b/crates/recent_projects/Cargo.toml @@ -9,17 +9,17 @@ path = "src/recent_projects.rs" doctest = false [dependencies] -db = { path = "../db" } -editor = { path = "../editor" } -fuzzy = { path = "../fuzzy" } -gpui = { path = "../gpui" } -language = { path = "../language" } -picker = { path = "../picker" } -settings = { path = "../settings" } -text = { path = "../text" } +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +language = { package = "language2", path = "../language2" } +picker = { package = "picker2", path = "../picker2" } +settings = { package = "settings2", path = "../settings2" } +text = { package = "text2", path = "../text2" } util = { path = "../util"} -theme = { path = "../theme" } -workspace = { path = "../workspace" } +theme = { package = "theme2", path = "../theme2" } +ui = { package = "ui2", path = "../ui2" } +workspace = { package = "workspace2", path = "../workspace2" } futures.workspace = true ordered-float.workspace = true @@ -27,4 +27,4 @@ postage.workspace = true smol.workspace = true [dev-dependencies] -editor = { path = "../editor", features = ["test-support"] } +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/recent_projects/src/highlighted_workspace_location.rs b/crates/recent_projects/src/highlighted_workspace_location.rs index 79c7ddec18..b31dabe8bd 100644 --- a/crates/recent_projects/src/highlighted_workspace_location.rs +++ b/crates/recent_projects/src/highlighted_workspace_location.rs @@ -1,13 +1,11 @@ use std::path::Path; use fuzzy::StringMatch; -use gpui::{ - elements::{Label, LabelStyle}, - AnyElement, Element, -}; +use ui::{prelude::*, HighlightedLabel}; use util::paths::PathExt; use workspace::WorkspaceLocation; +#[derive(IntoElement)] pub struct HighlightedText { pub text: String, pub highlight_positions: Vec, @@ -42,11 +40,11 @@ impl HighlightedText { char_count, } } +} - pub fn render(self, style: impl Into) -> AnyElement { - Label::new(self.text, style) - .with_highlights(self.highlight_positions) - .into_any() +impl RenderOnce for HighlightedText { + fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + HighlightedLabel::new(self.text, self.highlight_positions) } } diff --git a/crates/recent_projects2/src/projects.rs b/crates/recent_projects/src/projects.rs similarity index 100% rename from crates/recent_projects2/src/projects.rs rename to crates/recent_projects/src/projects.rs diff --git a/crates/recent_projects/src/recent_projects.rs b/crates/recent_projects/src/recent_projects.rs index 7a09ac259f..c3b2c21d52 100644 --- a/crates/recent_projects/src/recent_projects.rs +++ b/crates/recent_projects/src/recent_projects.rs @@ -1,86 +1,122 @@ mod highlighted_workspace_location; +mod projects; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ - actions, - anyhow::Result, - elements::{Flex, ParentElement}, - AnyElement, AppContext, Element, Task, ViewContext, WeakViewHandle, + AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task, + View, ViewContext, WeakView, }; use highlighted_workspace_location::HighlightedWorkspaceLocation; use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate, PickerEvent}; +use picker::{Picker, PickerDelegate}; use std::sync::Arc; +use ui::{prelude::*, ListItem, ListItemSpacing}; use util::paths::PathExt; -use workspace::{ - notifications::simple_message_notification::MessageNotification, Workspace, WorkspaceLocation, - WORKSPACE_DB, -}; +use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB}; -actions!(projects, [OpenRecent]); +pub use projects::OpenRecent; pub fn init(cx: &mut AppContext) { - cx.add_async_action(toggle); - RecentProjects::init(cx); + cx.observe_new_views(RecentProjects::register).detach(); } -fn toggle( - _: &mut Workspace, - _: &OpenRecent, - cx: &mut ViewContext, -) -> Option>> { - Some(cx.spawn(|workspace, mut cx| async move { - let workspace_locations: Vec<_> = cx - .background() - .spawn(async { - WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .unwrap_or_default() - .into_iter() - .map(|(_, location)| location) - .collect() - }) - .await; +pub struct RecentProjects { + pub picker: View>, + rem_width: f32, + _subscription: Subscription, +} - workspace.update(&mut cx, |workspace, cx| { - if !workspace_locations.is_empty() { - workspace.toggle_modal(cx, |_, cx| { - let workspace = cx.weak_handle(); - cx.add_view(|cx| { - RecentProjects::new( - RecentProjectsDelegate::new(workspace, workspace_locations, true), - cx, - ) - .with_max_size(800., 1200.) - }) - }); - } else { - workspace.show_notification(0, cx, |cx| { - cx.add_view(|_| MessageNotification::new("No recent projects to open.")) +impl ModalView for RecentProjects {} + +impl RecentProjects { + fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext) -> Self { + let picker = cx.new_view(|cx| Picker::new(delegate, cx)); + let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); + // We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap + // out workspace locations once the future runs to completion. + cx.spawn(|this, mut cx| async move { + let workspaces = WORKSPACE_DB + .recent_workspaces_on_disk() + .await + .unwrap_or_default() + .into_iter() + .map(|(_, location)| location) + .collect(); + this.update(&mut cx, move |this, cx| { + this.picker.update(cx, move |picker, cx| { + picker.delegate.workspace_locations = workspaces; + picker.update_matches(picker.query(cx), cx) }) - } - })?; - Ok(()) - })) + }) + .ok() + }) + .detach(); + Self { + picker, + rem_width, + _subscription, + } + } + + fn register(workspace: &mut Workspace, _: &mut ViewContext) { + workspace.register_action(|workspace, _: &OpenRecent, cx| { + let Some(recent_projects) = workspace.active_modal::(cx) else { + if let Some(handler) = Self::open(workspace, cx) { + handler.detach_and_log_err(cx); + } + return; + }; + + recent_projects.update(cx, |recent_projects, cx| { + recent_projects + .picker + .update(cx, |picker, cx| picker.cycle_selection(cx)) + }); + }); + } + + fn open(_: &mut Workspace, cx: &mut ViewContext) -> Option>> { + Some(cx.spawn(|workspace, mut cx| async move { + workspace.update(&mut cx, |workspace, cx| { + let weak_workspace = cx.view().downgrade(); + workspace.toggle_modal(cx, |cx| { + let delegate = RecentProjectsDelegate::new(weak_workspace, true); + + let modal = RecentProjects::new(delegate, 34., cx); + modal + }); + })?; + Ok(()) + })) + } + pub fn open_popover(workspace: WeakView, cx: &mut WindowContext<'_>) -> View { + cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx)) + } } -pub fn build_recent_projects( - workspace: WeakViewHandle, - workspaces: Vec, - cx: &mut ViewContext, -) -> RecentProjects { - Picker::new( - RecentProjectsDelegate::new(workspace, workspaces, false), - cx, - ) - .with_theme(|theme| theme.picker.clone()) +impl EventEmitter for RecentProjects {} + +impl FocusableView for RecentProjects { + fn focus_handle(&self, cx: &AppContext) -> FocusHandle { + self.picker.focus_handle(cx) + } } -pub type RecentProjects = Picker; +impl Render for RecentProjects { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + v_stack() + .w(rems(self.rem_width)) + .child(self.picker.clone()) + .on_mouse_down_out(cx.listener(|this, _, cx| { + this.picker.update(cx, |this, cx| { + this.cancel(&Default::default(), cx); + }) + })) + } +} pub struct RecentProjectsDelegate { - workspace: WeakViewHandle, + workspace: WeakView, workspace_locations: Vec, selected_match_index: usize, matches: Vec, @@ -88,22 +124,20 @@ pub struct RecentProjectsDelegate { } impl RecentProjectsDelegate { - fn new( - workspace: WeakViewHandle, - workspace_locations: Vec, - render_paths: bool, - ) -> Self { + fn new(workspace: WeakView, render_paths: bool) -> Self { Self { workspace, - workspace_locations, + workspace_locations: vec![], selected_match_index: 0, matches: Default::default(), render_paths, } } } - +impl EventEmitter for RecentProjectsDelegate {} impl PickerDelegate for RecentProjectsDelegate { + type ListItem = ListItem; + fn placeholder_text(&self) -> Arc { "Recent Projects...".into() } @@ -116,14 +150,14 @@ impl PickerDelegate for RecentProjectsDelegate { self.selected_match_index } - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext) { + fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { self.selected_match_index = ix; } fn update_matches( &mut self, query: String, - cx: &mut ViewContext, + cx: &mut ViewContext>, ) -> gpui::Task<()> { let query = query.trim_start(); let smart_case = query.chars().any(|c| c.is_uppercase()); @@ -147,7 +181,7 @@ impl PickerDelegate for RecentProjectsDelegate { smart_case, 100, &Default::default(), - cx.background().clone(), + cx.background_executor().clone(), )); self.matches.sort_unstable_by_key(|m| m.candidate_id); @@ -162,11 +196,11 @@ impl PickerDelegate for RecentProjectsDelegate { Task::ready(()) } - fn confirm(&mut self, _: bool, cx: &mut ViewContext) { + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { if let Some((selected_match, workspace)) = self .matches .get(self.selected_index()) - .zip(self.workspace.upgrade(cx)) + .zip(self.workspace.upgrade()) { let workspace_location = &self.workspace_locations[selected_match.candidate_id]; workspace @@ -175,41 +209,39 @@ impl PickerDelegate for RecentProjectsDelegate { .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx) }) .detach_and_log_err(cx); - cx.emit(PickerEvent::Dismiss); + cx.emit(DismissEvent); } } - fn dismissed(&mut self, _cx: &mut ViewContext) {} + fn dismissed(&mut self, _: &mut ViewContext>) {} fn render_match( &self, ix: usize, - mouse_state: &mut gpui::MouseState, selected: bool, - cx: &gpui::AppContext, - ) -> AnyElement> { - let theme = theme::current(cx); - let style = theme.picker.item.in_state(selected).style_for(mouse_state); - - let string_match = &self.matches[ix]; + _cx: &mut ViewContext>, + ) -> Option { + let Some(r#match) = self.matches.get(ix) else { + return None; + }; let highlighted_location = HighlightedWorkspaceLocation::new( - &string_match, - &self.workspace_locations[string_match.candidate_id], + &r#match, + &self.workspace_locations[r#match.candidate_id], ); - Flex::column() - .with_child(highlighted_location.names.render(style.label.clone())) - .with_children( - highlighted_location - .paths - .into_iter() - .filter(|_| self.render_paths) - .map(|highlighted_path| highlighted_path.render(style.label.clone())), - ) - .flex(1., false) - .contained() - .with_style(style.container) - .into_any_named("match") + Some( + ListItem::new(ix) + .inset(true) + .spacing(ListItemSpacing::Sparse) + .selected(selected) + .child( + v_stack() + .child(highlighted_location.names) + .when(self.render_paths, |this| { + this.children(highlighted_location.paths) + }), + ), + ) } } diff --git a/crates/recent_projects2/Cargo.toml b/crates/recent_projects2/Cargo.toml deleted file mode 100644 index 46854ab4e8..0000000000 --- a/crates/recent_projects2/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "recent_projects2" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/recent_projects.rs" -doctest = false - -[dependencies] -editor = { package = "editor2", path = "../editor2" } -fuzzy = { package = "fuzzy2", path = "../fuzzy2" } -gpui = { package = "gpui2", path = "../gpui2" } -language = { package = "language2", path = "../language2" } -picker = { package = "picker2", path = "../picker2" } -settings = { package = "settings2", path = "../settings2" } -text = { package = "text2", path = "../text2" } -util = { path = "../util"} -theme = { package = "theme2", path = "../theme2" } -ui = { package = "ui2", path = "../ui2" } -workspace = { package = "workspace2", path = "../workspace2" } - -futures.workspace = true -ordered-float.workspace = true -postage.workspace = true -smol.workspace = true - -[dev-dependencies] -editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/recent_projects2/src/highlighted_workspace_location.rs b/crates/recent_projects2/src/highlighted_workspace_location.rs deleted file mode 100644 index b31dabe8bd..0000000000 --- a/crates/recent_projects2/src/highlighted_workspace_location.rs +++ /dev/null @@ -1,129 +0,0 @@ -use std::path::Path; - -use fuzzy::StringMatch; -use ui::{prelude::*, HighlightedLabel}; -use util::paths::PathExt; -use workspace::WorkspaceLocation; - -#[derive(IntoElement)] -pub struct HighlightedText { - pub text: String, - pub highlight_positions: Vec, - char_count: usize, -} - -impl HighlightedText { - fn join(components: impl Iterator, separator: &str) -> Self { - let mut char_count = 0; - let separator_char_count = separator.chars().count(); - let mut text = String::new(); - let mut highlight_positions = Vec::new(); - for component in components { - if char_count != 0 { - text.push_str(separator); - char_count += separator_char_count; - } - - highlight_positions.extend( - component - .highlight_positions - .iter() - .map(|position| position + char_count), - ); - text.push_str(&component.text); - char_count += component.text.chars().count(); - } - - Self { - text, - highlight_positions, - char_count, - } - } -} - -impl RenderOnce for HighlightedText { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { - HighlightedLabel::new(self.text, self.highlight_positions) - } -} - -pub struct HighlightedWorkspaceLocation { - pub names: HighlightedText, - pub paths: Vec, -} - -impl HighlightedWorkspaceLocation { - pub fn new(string_match: &StringMatch, location: &WorkspaceLocation) -> Self { - let mut path_start_offset = 0; - let (names, paths): (Vec<_>, Vec<_>) = location - .paths() - .iter() - .map(|path| { - let path = path.compact(); - let highlighted_text = Self::highlights_for_path( - path.as_ref(), - &string_match.positions, - path_start_offset, - ); - - path_start_offset += highlighted_text.1.char_count; - - highlighted_text - }) - .unzip(); - - Self { - names: HighlightedText::join(names.into_iter().filter_map(|name| name), ", "), - paths, - } - } - - // Compute the highlighted text for the name and path - fn highlights_for_path( - path: &Path, - match_positions: &Vec, - path_start_offset: usize, - ) -> (Option, HighlightedText) { - let path_string = path.to_string_lossy(); - let path_char_count = path_string.chars().count(); - // Get the subset of match highlight positions that line up with the given path. - // Also adjusts them to start at the path start - let path_positions = match_positions - .iter() - .copied() - .skip_while(|position| *position < path_start_offset) - .take_while(|position| *position < path_start_offset + path_char_count) - .map(|position| position - path_start_offset) - .collect::>(); - - // Again subset the highlight positions to just those that line up with the file_name - // again adjusted to the start of the file_name - let file_name_text_and_positions = path.file_name().map(|file_name| { - let text = file_name.to_string_lossy(); - let char_count = text.chars().count(); - let file_name_start = path_char_count - char_count; - let highlight_positions = path_positions - .iter() - .copied() - .skip_while(|position| *position < file_name_start) - .take_while(|position| *position < file_name_start + char_count) - .map(|position| position - file_name_start) - .collect::>(); - HighlightedText { - text: text.to_string(), - highlight_positions, - char_count, - } - }); - - ( - file_name_text_and_positions, - HighlightedText { - text: path_string.to_string(), - highlight_positions: path_positions, - char_count: path_char_count, - }, - ) - } -} diff --git a/crates/recent_projects2/src/recent_projects.rs b/crates/recent_projects2/src/recent_projects.rs deleted file mode 100644 index c3b2c21d52..0000000000 --- a/crates/recent_projects2/src/recent_projects.rs +++ /dev/null @@ -1,247 +0,0 @@ -mod highlighted_workspace_location; -mod projects; - -use fuzzy::{StringMatch, StringMatchCandidate}; -use gpui::{ - AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Result, Subscription, Task, - View, ViewContext, WeakView, -}; -use highlighted_workspace_location::HighlightedWorkspaceLocation; -use ordered_float::OrderedFloat; -use picker::{Picker, PickerDelegate}; -use std::sync::Arc; -use ui::{prelude::*, ListItem, ListItemSpacing}; -use util::paths::PathExt; -use workspace::{ModalView, Workspace, WorkspaceLocation, WORKSPACE_DB}; - -pub use projects::OpenRecent; - -pub fn init(cx: &mut AppContext) { - cx.observe_new_views(RecentProjects::register).detach(); -} - -pub struct RecentProjects { - pub picker: View>, - rem_width: f32, - _subscription: Subscription, -} - -impl ModalView for RecentProjects {} - -impl RecentProjects { - fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext) -> Self { - let picker = cx.new_view(|cx| Picker::new(delegate, cx)); - let _subscription = cx.subscribe(&picker, |_, _, _, cx| cx.emit(DismissEvent)); - // We do not want to block the UI on a potentially lenghty call to DB, so we're gonna swap - // out workspace locations once the future runs to completion. - cx.spawn(|this, mut cx| async move { - let workspaces = WORKSPACE_DB - .recent_workspaces_on_disk() - .await - .unwrap_or_default() - .into_iter() - .map(|(_, location)| location) - .collect(); - this.update(&mut cx, move |this, cx| { - this.picker.update(cx, move |picker, cx| { - picker.delegate.workspace_locations = workspaces; - picker.update_matches(picker.query(cx), cx) - }) - }) - .ok() - }) - .detach(); - Self { - picker, - rem_width, - _subscription, - } - } - - fn register(workspace: &mut Workspace, _: &mut ViewContext) { - workspace.register_action(|workspace, _: &OpenRecent, cx| { - let Some(recent_projects) = workspace.active_modal::(cx) else { - if let Some(handler) = Self::open(workspace, cx) { - handler.detach_and_log_err(cx); - } - return; - }; - - recent_projects.update(cx, |recent_projects, cx| { - recent_projects - .picker - .update(cx, |picker, cx| picker.cycle_selection(cx)) - }); - }); - } - - fn open(_: &mut Workspace, cx: &mut ViewContext) -> Option>> { - Some(cx.spawn(|workspace, mut cx| async move { - workspace.update(&mut cx, |workspace, cx| { - let weak_workspace = cx.view().downgrade(); - workspace.toggle_modal(cx, |cx| { - let delegate = RecentProjectsDelegate::new(weak_workspace, true); - - let modal = RecentProjects::new(delegate, 34., cx); - modal - }); - })?; - Ok(()) - })) - } - pub fn open_popover(workspace: WeakView, cx: &mut WindowContext<'_>) -> View { - cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx)) - } -} - -impl EventEmitter for RecentProjects {} - -impl FocusableView for RecentProjects { - fn focus_handle(&self, cx: &AppContext) -> FocusHandle { - self.picker.focus_handle(cx) - } -} - -impl Render for RecentProjects { - fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { - v_stack() - .w(rems(self.rem_width)) - .child(self.picker.clone()) - .on_mouse_down_out(cx.listener(|this, _, cx| { - this.picker.update(cx, |this, cx| { - this.cancel(&Default::default(), cx); - }) - })) - } -} - -pub struct RecentProjectsDelegate { - workspace: WeakView, - workspace_locations: Vec, - selected_match_index: usize, - matches: Vec, - render_paths: bool, -} - -impl RecentProjectsDelegate { - fn new(workspace: WeakView, render_paths: bool) -> Self { - Self { - workspace, - workspace_locations: vec![], - selected_match_index: 0, - matches: Default::default(), - render_paths, - } - } -} -impl EventEmitter for RecentProjectsDelegate {} -impl PickerDelegate for RecentProjectsDelegate { - type ListItem = ListItem; - - fn placeholder_text(&self) -> Arc { - "Recent Projects...".into() - } - - fn match_count(&self) -> usize { - self.matches.len() - } - - fn selected_index(&self) -> usize { - self.selected_match_index - } - - fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext>) { - self.selected_match_index = ix; - } - - fn update_matches( - &mut self, - query: String, - cx: &mut ViewContext>, - ) -> gpui::Task<()> { - let query = query.trim_start(); - let smart_case = query.chars().any(|c| c.is_uppercase()); - let candidates = self - .workspace_locations - .iter() - .enumerate() - .map(|(id, location)| { - let combined_string = location - .paths() - .iter() - .map(|path| path.compact().to_string_lossy().into_owned()) - .collect::>() - .join(""); - StringMatchCandidate::new(id, combined_string) - }) - .collect::>(); - self.matches = smol::block_on(fuzzy::match_strings( - candidates.as_slice(), - query, - smart_case, - 100, - &Default::default(), - cx.background_executor().clone(), - )); - self.matches.sort_unstable_by_key(|m| m.candidate_id); - - self.selected_match_index = self - .matches - .iter() - .enumerate() - .rev() - .max_by_key(|(_, m)| OrderedFloat(m.score)) - .map(|(ix, _)| ix) - .unwrap_or(0); - Task::ready(()) - } - - fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { - if let Some((selected_match, workspace)) = self - .matches - .get(self.selected_index()) - .zip(self.workspace.upgrade()) - { - let workspace_location = &self.workspace_locations[selected_match.candidate_id]; - workspace - .update(cx, |workspace, cx| { - workspace - .open_workspace_for_paths(workspace_location.paths().as_ref().clone(), cx) - }) - .detach_and_log_err(cx); - cx.emit(DismissEvent); - } - } - - fn dismissed(&mut self, _: &mut ViewContext>) {} - - fn render_match( - &self, - ix: usize, - selected: bool, - _cx: &mut ViewContext>, - ) -> Option { - let Some(r#match) = self.matches.get(ix) else { - return None; - }; - - let highlighted_location = HighlightedWorkspaceLocation::new( - &r#match, - &self.workspace_locations[r#match.candidate_id], - ); - - Some( - ListItem::new(ix) - .inset(true) - .spacing(ListItemSpacing::Sparse) - .selected(selected) - .child( - v_stack() - .child(highlighted_location.names) - .when(self.render_paths, |this| { - this.children(highlighted_location.paths) - }), - ), - ) - } -} diff --git a/crates/zed/Cargo.toml b/crates/zed/Cargo.toml index fd235a318a..3b854902a3 100644 --- a/crates/zed/Cargo.toml +++ b/crates/zed/Cargo.toml @@ -17,9 +17,9 @@ path = "src/main.rs" [dependencies] ai = { package = "ai2", path = "../ai2"} audio = { package = "audio2", path = "../audio2" } -activity_indicator = { package = "activity_indicator2", path = "../activity_indicator2"} +activity_indicator = { path = "../activity_indicator"} auto_update = { package = "auto_update2", path = "../auto_update2" } -breadcrumbs = { package = "breadcrumbs2", path = "../breadcrumbs2" } +breadcrumbs = { path = "../breadcrumbs" } call = { package = "call2", path = "../call2" } channel = { package = "channel2", path = "../channel2" } cli = { path = "../cli" } @@ -30,7 +30,7 @@ command_palette = { path = "../command_palette" } client = { package = "client2", path = "../client2" } # clock = { path = "../clock" } copilot = { package = "copilot2", path = "../copilot2" } -copilot_button = { package = "copilot_button2", path = "../copilot_button2" } +copilot_button = { path = "../copilot_button" } diagnostics = { path = "../diagnostics" } db = { package = "db2", path = "../db2" } editor = { package="editor2", path = "../editor2" } @@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" } install_cli = { package = "install_cli2", path = "../install_cli2" } journal = { package = "journal2", path = "../journal2" } language = { package = "language2", path = "../language2" } -language_selector = { package = "language_selector2", path = "../language_selector2" } +language_selector = { path = "../language_selector" } lsp = { package = "lsp2", path = "../lsp2" } menu = { package = "menu2", path = "../menu2" } language_tools = { package = "language_tools2", path = "../language_tools2" } @@ -54,10 +54,10 @@ assistant = { package = "assistant2", path = "../assistant2" } outline = { package = "outline2", path = "../outline2" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } -project_panel = { package = "project_panel2", path = "../project_panel2" } +project_panel = { path = "../project_panel" } project_symbols = { path = "../project_symbols" } quick_action_bar = { path = "../quick_action_bar" } -recent_projects = { package = "recent_projects2", path = "../recent_projects2" } +recent_projects = { path = "../recent_projects" } rope = { package = "rope2", path = "../rope2"} rpc = { package = "rpc2", path = "../rpc2" } settings = { package = "settings2", path = "../settings2" }