Detailed changes
@@ -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",
@@ -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",
@@ -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"] }
@@ -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<LspStatus>,
- project: ModelHandle<Project>,
- auto_updater: Option<ModelHandle<AutoUpdater>>,
+ project: Model<Project>,
+ auto_updater: Option<Model<AutoUpdater>>,
}
struct LspStatus {
@@ -47,20 +47,15 @@ struct Content {
on_click: Option<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
}
-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<LanguageRegistry>,
cx: &mut ViewContext<Workspace>,
- ) -> ViewHandle<ActivityIndicator> {
+ ) -> View<ActivityIndicator> {
let project = workspace.project().clone();
let auto_updater = AutoUpdater::get(cx);
- let this = cx.add_view(|cx: &mut ViewContext<Self>| {
+ let this = cx.new_view(|cx: &mut ViewContext<Self>| {
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<Event> for ActivityIndicator {}
-impl View for ActivityIndicator {
- fn ui_name() -> &'static str {
- "ActivityIndicator"
- }
+impl Render for ActivityIndicator {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
+ let content = self.content_to_render(cx);
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let Content {
- icon,
- message,
- on_click,
- } = self.content_to_render(cx);
-
- let mut element = MouseEventHandler::new::<Self, _>(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")
- }))
- .with_child(
- Text::new(message, style.message.clone())
- .with_soft_wrap(false)
- .aligned(),
- )
- .constrained()
- .with_height(style.height)
- .contained()
- .with_style(style.container)
- .aligned()
- });
+ 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) = on_click.clone() {
- element = element
- .with_cursor_style(CursorStyle::PointingHand)
- .on_click(MouseButton::Left, move |_, this, cx| on_click(this, cx));
+ if let Some(on_click) = content.on_click {
+ result = result
+ .cursor(CursorStyle::PointingHand)
+ .on_click(cx.listener(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))
}
}
@@ -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"] }
@@ -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<str>, error: String },
-}
-
-pub struct ActivityIndicator {
- statuses: Vec<LspStatus>,
- project: Model<Project>,
- auto_updater: Option<Model<AutoUpdater>>,
-}
-
-struct LspStatus {
- name: Arc<str>,
- 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<Arc<dyn Fn(&mut ActivityIndicator, &mut ViewContext<ActivityIndicator>)>>,
-}
-
-impl ActivityIndicator {
- pub fn new(
- workspace: &mut Workspace,
- languages: Arc<LanguageRegistry>,
- cx: &mut ViewContext<Workspace>,
- ) -> View<ActivityIndicator> {
- let project = workspace.project().clone();
- let auto_updater = AutoUpdater::get(cx);
- let this = cx.new_view(|cx: &mut ViewContext<Self>| {
- 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>) {
- 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<Self>) {
- 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<Item = PendingWork<'a>> {
- 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::<SmallVec<[_; 4]>>();
- 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<Self>) -> 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<Event> for ActivityIndicator {}
-
-impl Render for ActivityIndicator {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> 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<Self>) {}
-}
@@ -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"] }
@@ -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<Box<dyn ItemHandle>>,
- project_search: Option<ViewHandle<ProjectSearchView>>,
subscription: Option<Subscription>,
- workspace: WeakViewHandle<Workspace>,
}
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 View for Breadcrumbs {
- fn ui_name() -> &'static str {
- "Breadcrumbs"
- }
+impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- 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<Self>) -> 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::<editor::Editor>().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::<Editor>()
+ .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::<Breadcrumbs, _>(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::<Breadcrumbs>(
- 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>) {
self.pane_focused = pane_focused;
}
@@ -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"] }
@@ -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<Box<dyn ItemHandle>>,
- subscription: Option<Subscription>,
-}
-
-impl Breadcrumbs {
- pub fn new() -> Self {
- Self {
- pane_focused: false,
- active_item: Default::default(),
- subscription: Default::default(),
- }
- }
-}
-
-impl EventEmitter<ToolbarItemEvent> for Breadcrumbs {}
-
-impl Render for Breadcrumbs {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> 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::<Editor>()
- .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<Self>,
- ) -> 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>) {
- self.pane_focused = pane_focused;
- }
-}
@@ -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"}
@@ -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"] }
@@ -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<ContextMenu>,
editor_subscription: Option<(Subscription, usize)>,
editor_enabled: Option<bool>,
language: Option<Arc<Language>>,
@@ -34,25 +33,15 @@ pub struct CopilotButton {
fs: Arc<dyn Fs>,
}
-impl Entity for CopilotButton {
- type Event = ();
-}
-
-impl View for CopilotButton {
- fn ui_name() -> &'static str {
- "CopilotButton"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
+impl Render for CopilotButton {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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::<Self, _>(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);
-
- 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>()
- {
- workspace.update(cx, |workspace, cx| {
+ 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>() {
+ 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::<Self>(
- 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<dyn Fs>, cx: &mut ViewContext<Self>) -> 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::<SettingsStore, _>(move |_, cx| cx.notify())
+ cx.observe_global::<SettingsStore>(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<Self>) {
- let mut menu_options = Vec::with_capacity(2);
+ pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
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<Self>) {
+ pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
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),
- ));
- }
- let settings = settings::get::<AllLanguageSettings>(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;
+
+ 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::<Workspace>() {
- 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);
- 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,
- );
+ 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::<Workspace>() {
+ 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: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+ pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
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<Self>) {
if let Some(editor) = item.map(|item| item.act_as::<Editor>(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>,
+ workspace: WeakView<Workspace>,
path_to_disable: Option<Arc<Path>>,
- 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::<Workspace>() else {
+ let Some(workspace) = cx.window_handle().downcast::<Workspace>() 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(
@@ -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"] }
@@ -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<bool>,
- language: Option<Arc<Language>>,
- file: Option<Arc<dyn File>>,
- fs: Arc<dyn Fs>,
-}
-
-impl Render for CopilotButton {
- fn render(&mut self, cx: &mut ViewContext<Self>) -> 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>() {
- 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<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
- Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
-
- cx.observe_global::<SettingsStore>(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<Self>) -> View<ContextMenu> {
- 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<Self>) -> View<ContextMenu> {
- 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::<Workspace>() {
- 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<Editor>, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- if let Some(editor) = item.map(|item| item.act_as::<Editor>(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<Workspace>,
- path_to_disable: Option<Arc<Path>>,
- 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::<Editor>()
- .unwrap();
-
- settings_editor.downgrade().update(&mut cx, |item, cx| {
- let text = item.buffer().read(cx).snapshot(cx).text();
-
- let settings = cx.global::<SettingsStore>();
- let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
- let copilot = file.copilot.get_or_insert_with(Default::default);
- let globs = copilot.disabled_globs.get_or_insert_with(|| {
- settings
- .get::<AllLanguageSettings>(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<dyn Fs>, cx: &mut AppContext) {
- let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
- update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
- file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
- });
-}
-
-fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
- let show_copilot_suggestions =
- all_language_settings(None, cx).copilot_enabled(Some(&language), None);
- update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
- file.languages
- .entry(language.name())
- .or_default()
- .show_copilot_suggestions = Some(!show_copilot_suggestions);
- });
-}
-
-fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
- update_settings_file::<AllLanguageSettings>(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::<Workspace>() 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);
- }
- }
-}
@@ -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"] }
@@ -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<Option<Arc<str>>>,
- workspace: WeakViewHandle<Workspace>,
+ workspace: WeakView<Workspace>,
_observe_active_editor: Option<Subscription>,
}
@@ -22,7 +21,7 @@ impl ActiveBufferLanguage {
}
}
- fn update_language(&mut self, editor: ViewHandle<Editor>, cx: &mut ViewContext<Self>) {
+ fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
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<Self>) -> AnyElement<Self> {
- if let Some(active_language) = self.active_language.as_ref() {
+impl Render for ActiveBufferLanguage {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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::<Self, _>(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::<Self>(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)),
+ )
+ })
}
}
@@ -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::<LanguageSelectorDelegate>::init(cx);
- cx.add_action(toggle);
+ cx.observe_new_views(LanguageSelector::register).detach();
}
-pub fn toggle(
- workspace: &mut Workspace,
- _: &Toggle,
- cx: &mut ViewContext<Workspace>,
-) -> Option<()> {
- let (_, buffer, _) = workspace
- .active_item(cx)?
- .act_as::<Editor>(cx)?
- .read(cx)
- .active_excerpt(cx)?;
- workspace.toggle_modal(cx, |workspace, cx| {
+pub struct LanguageSelector {
+ picker: View<Picker<LanguageSelectorDelegate>>,
+}
+
+impl LanguageSelector {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(move |workspace, _: &Toggle, cx| {
+ Self::toggle(workspace, cx);
+ });
+ }
+
+ fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
let registry = workspace.app_state().languages.clone();
- cx.add_view(|cx| {
- Picker::new(
- LanguageSelectorDelegate::new(buffer, workspace.project().clone(), registry),
- cx,
- )
- })
- });
- Some(())
+ let (_, buffer, _) = workspace
+ .active_item(cx)?
+ .act_as::<Editor>(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<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut ViewContext<Self>,
+ ) -> 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<Self>) -> 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<DismissEvent> for LanguageSelector {}
+impl ModalView for LanguageSelector {}
+
pub struct LanguageSelectorDelegate {
- buffer: ModelHandle<Buffer>,
- project: ModelHandle<Project>,
+ language_selector: WeakView<LanguageSelector>,
+ buffer: Model<Buffer>,
+ project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
candidates: Vec<StringMatchCandidate>,
matches: Vec<StringMatch>,
@@ -52,8 +93,9 @@ pub struct LanguageSelectorDelegate {
impl LanguageSelectorDelegate {
fn new(
- buffer: ModelHandle<Buffer>,
- project: ModelHandle<Project>,
+ language_selector: WeakView<LanguageSelector>,
+ buffer: Model<Buffer>,
+ project: Model<Project>,
language_registry: Arc<LanguageRegistry>,
) -> Self {
let candidates = language_registry
@@ -62,29 +104,22 @@ impl LanguageSelectorDelegate {
.enumerate()
.map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
.collect::<Vec<_>>();
- let mut matches = candidates
- .iter()
- .map(|candidate| StringMatch {
- candidate_id: candidate.id,
- score: 0.,
- positions: Default::default(),
- string: candidate.string.clone(),
- })
- .collect::<Vec<_>>();
- 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<str> {
"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<Picker<Self>>) {}
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ 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<Picker<Self>>,
) -> 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<Picker<Self>> {
- let theme = theme::current(cx);
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
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())),
+ )
}
}
@@ -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"] }
@@ -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<Option<Arc<str>>>,
- workspace: WeakView<Workspace>,
- _observe_active_editor: Option<Subscription>,
-}
-
-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<Editor>, cx: &mut ViewContext<Self>) {
- 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<Self>) -> 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<Self>,
- ) {
- if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(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();
- }
-}
@@ -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<Picker<LanguageSelectorDelegate>>,
-}
-
-impl LanguageSelector {
- fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
- workspace.register_action(move |workspace, _: &Toggle, cx| {
- Self::toggle(workspace, cx);
- });
- }
-
- fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
- let registry = workspace.app_state().languages.clone();
- let (_, buffer, _) = workspace
- .active_item(cx)?
- .act_as::<Editor>(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<Buffer>,
- project: Model<Project>,
- language_registry: Arc<LanguageRegistry>,
- cx: &mut ViewContext<Self>,
- ) -> 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<Self>) -> 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<DismissEvent> for LanguageSelector {}
-impl ModalView for LanguageSelector {}
-
-pub struct LanguageSelectorDelegate {
- language_selector: WeakView<LanguageSelector>,
- buffer: Model<Buffer>,
- project: Model<Project>,
- language_registry: Arc<LanguageRegistry>,
- candidates: Vec<StringMatchCandidate>,
- matches: Vec<StringMatch>,
- selected_index: usize,
-}
-
-impl LanguageSelectorDelegate {
- fn new(
- language_selector: WeakView<LanguageSelector>,
- buffer: Model<Buffer>,
- project: Model<Project>,
- language_registry: Arc<LanguageRegistry>,
- ) -> Self {
- let candidates = language_registry
- .language_names()
- .into_iter()
- .enumerate()
- .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
- .collect::<Vec<_>>();
-
- 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<str> {
- "Select a language...".into()
- }
-
- fn match_count(&self) -> usize {
- self.matches.len()
- }
-
- fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
- 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<Picker<Self>>) {
- 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<Picker<Self>>) {
- self.selected_index = ix;
- }
-
- fn update_matches(
- &mut self,
- query: String,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> 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<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- 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())),
- )
- }
-}
@@ -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
@@ -41,56 +41,47 @@ impl FileAssociations {
})
}
- pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
- maybe!({
- let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+ pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
+ let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
- // FIXME: Associate a type with the languages and have the file's langauge
- // override these associations
- maybe!({
- 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()))
+ 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_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
- maybe!({
- let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+ pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+ let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
- let key = if expanded {
- EXPANDED_DIRECTORY_TYPE
- } else {
- COLLAPSED_DIRECTORY_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) -> Arc<str> {
- maybe!({
- let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
+ pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
+ let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
- let key = if expanded {
- EXPANDED_CHEVRON_TYPE
- } else {
- COLLAPSED_CHEVRON_TYPE
- };
+ let key = if expanded {
+ EXPANDED_CHEVRON_TYPE
+ } else {
+ COLLAPSED_CHEVRON_TYPE
+ };
- this.types
- .get(key)
- .map(|type_config| type_config.icon.clone())
- })
- .unwrap_or_else(|| Arc::from("".to_string()))
+ this.types
+ .get(key)
+ .map(|type_config| type_config.icon.clone())
}
}
@@ -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>,
+ project: Model<Project>,
fs: Arc<dyn Fs>,
- list: UniformListState,
+ list: UniformListScrollHandle,
+ focus_handle: FocusHandle,
visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
last_worktree_root_id: Option<ProjectEntryId>,
expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
selection: Option<Selection>,
+ context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
edit_state: Option<EditState>,
- filename_editor: ViewHandle<Editor>,
+ filename_editor: View<Editor>,
clipboard_entry: Option<ClipboardEntry>,
- context_menu: ViewHandle<ContextMenu>,
- dragged_entry_destination: Option<Arc<Path>>,
- workspace: WeakViewHandle<Workspace>,
- has_focus: bool,
- width: Option<f32>,
+ _dragged_entry_destination: Option<Arc<Path>>,
+ workspace: WeakView<Workspace>,
+ width: Option<Pixels>,
pending_serialization: Task<Option<()>>,
}
@@ -94,7 +87,7 @@ pub enum ClipboardEntry {
},
}
-#[derive(Debug, PartialEq, Eq)]
+#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EntryDetails {
filename: String,
icon: Option<Arc<str>>,
@@ -134,36 +127,19 @@ actions!(
);
pub fn init_settings(cx: &mut AppContext) {
- settings::register::<ProjectPanelSettings>(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<ProjectPanel>| {
- this.paste(action, cx);
- },
- );
+
+ cx.observe_new_views(|workspace: &mut Workspace, _| {
+ workspace.register_action(|workspace, _: &ToggleFocus, cx| {
+ workspace.toggle_panel_focus::<ProjectPanel>(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<f32>,
+ width: Option<Pixels>,
+}
+
+struct DraggedProjectEntryView {
+ entry_id: ProjectEntryId,
+ details: EntryDetails,
+ width: Pixels,
}
impl ProjectPanel {
- fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> ViewHandle<Self> {
+ fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
let project = workspace.project().clone();
- let project_panel = cx.add_view(|cx: &mut ViewContext<Self>| {
+ let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
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::<ProjectPanelSettings>(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);
}
- _ => {}
- })
- .detach();
- cx.observe_focus(&filename_editor, |this, _, is_focused, cx| {
- if !is_focused
- && this
+ 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);
+ {
+ this.edit_state = None;
+ this.update_visible_entries(None, cx);
+ }
}
+ _ => {}
})
.detach();
- cx.observe_global::<FileAssociations, _>(|_, cx| {
- cx.notify();
- })
- .detach();
+ // cx.observe_global::<FileAssociations, _>(|_, 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::<SettingsStore, _>(move |this, cx| {
+ ProjectPanelSettings::register(cx);
+ cx.observe_global::<SettingsStore>(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<Workspace>,
- cx: AsyncAppContext,
- ) -> Task<Result<ViewHandle<Self>>> {
- 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::<SerializedProjectPanel>(&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<Workspace>,
+ mut cx: AsyncWindowContext,
+ ) -> Result<View<Self>> {
+ 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::<SerializedProjectPanel>(&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<Self>) {
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<Self>) {
+ if !self.focus_handle.contains_focused(cx) {
+ cx.emit(Event::Focus);
+ }
+ }
+
fn deploy_context_menu(
&mut self,
- position: Vector2F,
+ position: Point<Pixels>,
entry_id: ProjectEntryId,
cx: &mut ViewContext<Self>,
) {
+ 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,60 +383,73 @@ 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));
- }));
+ 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_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 = 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_entries.push(ContextMenuItem::Separator);
- menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath));
- menu_entries.push(ContextMenuItem::action(
- "Copy Relative Path",
- CopyRelativePath,
- ));
- if entry.is_dir() {
- menu_entries.push(ContextMenuItem::Separator);
- }
- menu_entries.push(ContextMenuItem::action("Reveal in Finder", RevealInFinder));
- if entry.is_dir() {
- menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal));
- menu_entries.push(ContextMenuItem::action(
- "Search Inside",
- NewSearchInDirectory,
- ));
- }
+ menu = 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_entries.push(ContextMenuItem::Separator);
- menu_entries.push(ContextMenuItem::action("Rename", Rename));
- if !is_root {
- menu_entries.push(ContextMenuItem::action("Delete", Delete));
- }
- }
+ menu = menu.separator().action("Rename", Box::new(Rename));
- self.context_menu.update(cx, |menu, cx| {
- menu.show(position, AnchorCorner::TopLeft, menu_entries, cx);
- });
+ 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();
}
@@ -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<Self>) -> Option<Task<Result<()>>> {
+ fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
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<Self>) -> Option<Task<Result<()>>> {
+ fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
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<Self>) -> Option<Task<Result<()>>> {
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>) {
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<Workspace>, cx| {
- drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
- })
+ // cx.update_global(|drag_and_drop: &mut DragAndDrop<Workspace>, cx| {
+ // drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
+ // })
}
}
- fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) -> Option<Task<Result<()>>> {
- 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<Self>) {
+ 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<Self>) {
@@ -869,7 +862,7 @@ impl ProjectPanel {
fn autoscroll(&mut self, cx: &mut ViewContext<Self>) {
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<Self>) -> Option<()> {
- if let Some((worktree, entry)) = self.selected_entry(cx) {
+ fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
+ 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<Self>) {
@@ -976,24 +971,25 @@ impl ProjectPanel {
}
}
- fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext<Self>) {
- 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();
- }
+ fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext<Self>) {
+ 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(
@@ -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<Worktree>, &'a project::Entry)> {
+ ) -> Option<(Model<Worktree>, &'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::<ProjectPanelSettings>(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<V: 'static>(
- details: &EntryDetails,
- editor: Option<&ViewHandle<Editor>>,
- padding: f32,
- row_container_style: ContainerStyle,
- style: &ProjectPanelEntry,
- cx: &mut ViewContext<V>,
- ) -> AnyElement<V> {
+ fn render_entry(
+ &self,
+ entry_id: ProjectEntryId,
+ details: EntryDetails,
+ cx: &mut ViewContext<Self>,
+ ) -> Stateful<Div> {
+ 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);
-
- 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)
+ .unwrap_or(if is_selected {
+ Color::Default
} else {
- Empty::new()
- .constrained()
- .with_max_width(style.icon_size)
- .with_max_height(style.icon_size)
- .aligned()
- .constrained()
- .with_width(style.icon_size)
+ 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,
+ })
})
- .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::<ProjectEntryId>(|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<Editor>,
- dragged_entry_destination: &mut Option<Arc<Path>>,
- theme: &theme::ProjectPanel,
- cx: &mut ViewContext<Self>,
- ) -> AnyElement<Self> {
- let kind = details.kind;
- let path = details.path.clone();
- let settings = settings::get::<ProjectPanelSettings>(cx);
- let padding = theme.container.padding.left + details.depth as f32 * settings.indent_size;
-
- let entry_style = if details.is_cut {
- &theme.cut_entry
- } else if details.is_ignored {
- &theme.ignored_entry
+ fn dispatch_context(&self, cx: &ViewContext<Self>) -> 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 {
- &theme.entry
+ "not_editing"
};
- let show_editor = details.is_editing && !details.is_processing;
-
- MouseEventHandler::new::<Self, _>(entry_id.to_usize(), cx, |state, cx| {
- let mut style = entry_style
- .in_state(details.is_selected)
- .style_for(state)
- .clone();
-
- if cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<ProjectEntryId>(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::<DragAndDrop<Workspace>>()
- .currently_dragged::<ProjectEntryId>(cx.window())
- {
- this.move_entry(
- *dragged_entry,
- entry_id,
- matches!(details.kind, EntryKind::File(_)),
- cx,
- );
- }
- })
- .on_move(move |_, this, cx| {
- if cx
- .global::<DragAndDrop<Workspace>>()
- .currently_dragged::<ProjectEntryId>(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<Workspace>| {
- 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>,
+ project: Model<Project>,
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);
@@ -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<bool>,
}
-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> {
Self::load_via_json_merge(default_value, user_values)
}
@@ -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
@@ -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<str>,
-}
-
-#[derive(Deserialize, Debug)]
-pub struct FileAssociations {
- suffixes: HashMap<String, String>,
- types: HashMap<String, TypeConfig>,
-}
-
-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::<FileAssociations>(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<Arc<str>> {
- let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
-
- // 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<Arc<str>> {
- let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
-
- 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<Arc<str>> {
- let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
-
- let key = if expanded {
- EXPANDED_CHEVRON_TYPE
- } else {
- COLLAPSED_CHEVRON_TYPE
- };
-
- this.types
- .get(key)
- .map(|type_config| type_config.icon.clone())
- }
-}
@@ -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<Project>,
- fs: Arc<dyn Fs>,
- list: UniformListScrollHandle,
- focus_handle: FocusHandle,
- visible_entries: Vec<(WorktreeId, Vec<Entry>)>,
- last_worktree_root_id: Option<ProjectEntryId>,
- expanded_dir_ids: HashMap<WorktreeId, Vec<ProjectEntryId>>,
- selection: Option<Selection>,
- context_menu: Option<(View<ContextMenu>, Point<Pixels>, Subscription)>,
- edit_state: Option<EditState>,
- filename_editor: View<Editor>,
- clipboard_entry: Option<ClipboardEntry>,
- _dragged_entry_destination: Option<Arc<Path>>,
- workspace: WeakView<Workspace>,
- width: Option<Pixels>,
- pending_serialization: Task<Option<()>>,
-}
-
-#[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<String>,
-}
-
-#[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<Arc<str>>,
- path: Arc<Path>,
- 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<GitFileStatus>,
-}
-
-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::<ProjectPanel>(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<Pixels>,
-}
-
-struct DraggedProjectEntryView {
- entry_id: ProjectEntryId,
- details: EntryDetails,
- width: Pixels,
-}
-
-impl ProjectPanel {
- fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
- let project = workspace.project().clone();
- let project_panel = cx.new_view(|cx: &mut ViewContext<Self>| {
- 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::<FileAssociations, _>(|_, 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::<SettingsStore>(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<Workspace>,
- mut cx: AsyncWindowContext,
- ) -> Result<View<Self>> {
- 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::<SerializedProjectPanel>(&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<Self>) {
- 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<Self>) {
- if !self.focus_handle.contains_focused(cx) {
- cx.emit(Event::Focus);
- }
- }
-
- fn deploy_context_menu(
- &mut self,
- position: Point<Pixels>,
- entry_id: ProjectEntryId,
- cx: &mut ViewContext<Self>,
- ) {
- 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<Self>) {
- 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<Self>) {
- 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>) {
- self.expanded_dir_ids.clear();
- self.update_visible_entries(None, cx);
- cx.notify();
- }
-
- fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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<Self>) {
- if let Some(task) = self.confirm_edit(cx) {
- task.detach_and_log_err(cx);
- }
- }
-
- fn open_file(&mut self, _: &Open, cx: &mut ViewContext<Self>) {
- 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<Self>) -> Option<Task<Result<()>>> {
- 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>) {
- 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<Self>,
- ) {
- cx.emit(Event::OpenedEntry {
- entry_id,
- focus_opened_item,
- });
- }
-
- fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext<Self>) {
- cx.emit(Event::SplitEntry { entry_id });
- }
-
- fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext<Self>) {
- self.add_entry(false, cx)
- }
-
- fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext<Self>) {
- self.add_entry(true, cx)
- }
-
- fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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<Workspace>, cx| {
- // drag_and_drop.cancel_dragging::<ProjectEntryId>(cx);
- // })
- }
- }
-
- fn delete(&mut self, _: &Delete, cx: &mut ViewContext<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>) {
- 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<Self>,
- ) {
- 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<Self>,
- ) {
- 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<Worktree>, &'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<Self>) -> 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<Self>,
- ) {
- 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>,
- ) {
- 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<usize>,
- cx: &mut ViewContext<ProjectPanel>,
- mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext<ProjectPanel>),
- ) {
- 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<Self>,
- ) -> Stateful<Div> {
- 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::<ProjectEntryId>(|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<Self>) -> 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<Project>,
- 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<Self>) -> 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<Self>) -> 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<Event> for ProjectPanel {}
-
-impl EventEmitter<PanelEvent> 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<Self>) {
- settings::update_settings_file::<ProjectPanelSettings>(
- 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<Pixels>, cx: &mut ViewContext<Self>) {
- self.width = size;
- self.serialize(cx);
- cx.notify();
- }
-
- fn icon(&self, _: &WindowContext) -> Option<ui::Icon> {
- Some(ui::Icon::FileTree)
- }
-
- fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
- Some("Project Panel")
- }
-
- fn toggle_action(&self) -> Box<dyn Action> {
- 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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<ProjectSettings>(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::<usize>(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::<usize>(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::<Vec<_>>();
- assert_eq!(active_items.len(), 1);
- let open_editor = active_items
- .into_iter()
- .next()
- .unwrap()
- .downcast::<Editor>()
- .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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
- project_settings.file_scan_exclusions = Some(Vec::new());
- });
- store.update_user_settings::<ProjectPanelSettings>(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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<ProjectPanelSettings>(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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<ProjectSettings>(cx, |project_settings| {
- project_settings.file_scan_exclusions = Some(Vec::new());
- });
- store.update_user_settings::<ProjectPanelSettings>(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<ProjectPanel>,
- path: impl AsRef<Path>,
- cx: &mut VisualTestContext,
- ) {
- let path = path.as_ref();
- panel.update(cx, |panel, cx| {
- for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
- 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<ProjectPanel>, path: impl AsRef<Path>, cx: &mut VisualTestContext) {
- let path = path.as_ref();
- panel.update(cx, |panel, cx| {
- for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
- 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<ProjectPanel>,
- path: impl AsRef<Path>,
- cx: &mut VisualTestContext,
- ) -> Option<ProjectEntryId> {
- let path = path.as_ref();
- panel.update(cx, |panel, cx| {
- for worktree in panel.project.read(cx).worktrees().collect::<Vec<_>>() {
- 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<ProjectPanel>,
- range: Range<usize>,
- cx: &mut VisualTestContext,
- ) -> Vec<String> {
- 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::<SettingsStore, _>(|store, cx| {
- store.update_user_settings::<ProjectSettings>(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<Workspace>,
- expected_path: &str,
- cx: &mut TestAppContext,
- ) {
- window
- .update(cx, |workspace, cx| {
- let worktrees = workspace.worktrees(cx).collect::<Vec<_>>();
- 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::<Vec<_>>();
- 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<ProjectPanel>, 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<Workspace>,
- 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::<Vec<_>>();
- assert!(
- open_project_paths.is_empty(),
- "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}"
- );
- })
- .unwrap();
- }
-}
@@ -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<f32>,
- pub dock: Option<ProjectPanelDockPosition>,
- pub file_icons: Option<bool>,
- pub folder_icons: Option<bool>,
- pub git_status: Option<bool>,
- pub indent_size: Option<f32>,
- pub auto_reveal_entries: Option<bool>,
-}
-
-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> {
- Self::load_via_json_merge(default_value, user_values)
- }
-}
@@ -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"] }
@@ -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<usize>,
@@ -42,11 +40,11 @@ impl HighlightedText {
char_count,
}
}
+}
- pub fn render<V: 'static>(self, style: impl Into<LabelStyle>) -> AnyElement<V> {
- 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)
}
}
@@ -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();
+}
+
+pub struct RecentProjects {
+ pub picker: View<Picker<RecentProjectsDelegate>>,
+ rem_width: f32,
+ _subscription: Subscription,
}
-fn toggle(
- _: &mut Workspace,
- _: &OpenRecent,
- cx: &mut ViewContext<Workspace>,
-) -> Option<Task<Result<()>>> {
- 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()
+impl ModalView for RecentProjects {}
+
+impl RecentProjects {
+ fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> 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)
+ })
})
- .await;
-
- 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.)
- })
+ .ok()
+ })
+ .detach();
+ Self {
+ picker,
+ rem_width,
+ _subscription,
+ }
+ }
+
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(|workspace, _: &OpenRecent, cx| {
+ let Some(recent_projects) = workspace.active_modal::<Self>(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<Workspace>) -> Option<Task<Result<()>>> {
+ 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
});
- } else {
- workspace.show_notification(0, cx, |cx| {
- cx.add_view(|_| MessageNotification::new("No recent projects to open."))
- })
- }
- })?;
- Ok(())
- }))
+ })?;
+ Ok(())
+ }))
+ }
+ pub fn open_popover(workspace: WeakView<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
+ cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
+ }
}
-pub fn build_recent_projects(
- workspace: WeakViewHandle<Workspace>,
- workspaces: Vec<WorkspaceLocation>,
- cx: &mut ViewContext<RecentProjects>,
-) -> RecentProjects {
- Picker::new(
- RecentProjectsDelegate::new(workspace, workspaces, false),
- cx,
- )
- .with_theme(|theme| theme.picker.clone())
+impl EventEmitter<DismissEvent> for RecentProjects {}
+
+impl FocusableView for RecentProjects {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
}
-pub type RecentProjects = Picker<RecentProjectsDelegate>;
+impl Render for RecentProjects {
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> 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>,
+ workspace: WeakView<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
@@ -88,22 +124,20 @@ pub struct RecentProjectsDelegate {
}
impl RecentProjectsDelegate {
- fn new(
- workspace: WeakViewHandle<Workspace>,
- workspace_locations: Vec<WorkspaceLocation>,
- render_paths: bool,
- ) -> Self {
+ fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
Self {
workspace,
- workspace_locations,
+ workspace_locations: vec![],
selected_match_index: 0,
matches: Default::default(),
render_paths,
}
}
}
-
+impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
impl PickerDelegate for RecentProjectsDelegate {
+ type ListItem = ListItem;
+
fn placeholder_text(&self) -> Arc<str> {
"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<RecentProjects>) {
+ fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
self.selected_match_index = ix;
}
fn update_matches(
&mut self,
query: String,
- cx: &mut ViewContext<RecentProjects>,
+ cx: &mut ViewContext<Picker<Self>>,
) -> 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<RecentProjects>) {
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
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<RecentProjects>) {}
+ fn dismissed(&mut self, _: &mut ViewContext<Picker<Self>>) {}
fn render_match(
&self,
ix: usize,
- mouse_state: &mut gpui::MouseState,
selected: bool,
- cx: &gpui::AppContext,
- ) -> AnyElement<Picker<Self>> {
- 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<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ 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)
+ }),
+ ),
+ )
}
}
@@ -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"] }
@@ -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<usize>,
- char_count: usize,
-}
-
-impl HighlightedText {
- fn join(components: impl Iterator<Item = Self>, 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<HighlightedText>,
-}
-
-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<usize>,
- path_start_offset: usize,
- ) -> (Option<HighlightedText>, 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::<Vec<_>>();
-
- // 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::<Vec<_>>();
- 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,
- },
- )
- }
-}
@@ -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<Picker<RecentProjectsDelegate>>,
- rem_width: f32,
- _subscription: Subscription,
-}
-
-impl ModalView for RecentProjects {}
-
-impl RecentProjects {
- fn new(delegate: RecentProjectsDelegate, rem_width: f32, cx: &mut ViewContext<Self>) -> 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>) {
- workspace.register_action(|workspace, _: &OpenRecent, cx| {
- let Some(recent_projects) = workspace.active_modal::<Self>(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<Workspace>) -> Option<Task<Result<()>>> {
- 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<Workspace>, cx: &mut WindowContext<'_>) -> View<Self> {
- cx.new_view(|cx| Self::new(RecentProjectsDelegate::new(workspace, false), 20., cx))
- }
-}
-
-impl EventEmitter<DismissEvent> 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<Self>) -> 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>,
- workspace_locations: Vec<WorkspaceLocation>,
- selected_match_index: usize,
- matches: Vec<StringMatch>,
- render_paths: bool,
-}
-
-impl RecentProjectsDelegate {
- fn new(workspace: WeakView<Workspace>, render_paths: bool) -> Self {
- Self {
- workspace,
- workspace_locations: vec![],
- selected_match_index: 0,
- matches: Default::default(),
- render_paths,
- }
- }
-}
-impl EventEmitter<DismissEvent> for RecentProjectsDelegate {}
-impl PickerDelegate for RecentProjectsDelegate {
- type ListItem = ListItem;
-
- fn placeholder_text(&self) -> Arc<str> {
- "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<Picker<Self>>) {
- self.selected_match_index = ix;
- }
-
- fn update_matches(
- &mut self,
- query: String,
- cx: &mut ViewContext<Picker<Self>>,
- ) -> 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::<Vec<_>>()
- .join("");
- StringMatchCandidate::new(id, combined_string)
- })
- .collect::<Vec<_>>();
- 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<Picker<Self>>) {
- 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<Picker<Self>>) {}
-
- fn render_match(
- &self,
- ix: usize,
- selected: bool,
- _cx: &mut ViewContext<Picker<Self>>,
- ) -> Option<Self::ListItem> {
- 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)
- }),
- ),
- )
- }
-}
@@ -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" }