From b9e098ead8ed508f02d2215876d1eb346e566877 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Thu, 9 Nov 2023 16:51:03 -0800 Subject: [PATCH 01/21] Start work on creating gpui2 version of project panel --- Cargo.lock | 31 + Cargo.toml | 1 + crates/project_panel2/Cargo.toml | 41 + .../project_panel2/src/file_associations.rs | 96 + crates/project_panel2/src/project_panel.rs | 2858 +++++++++++++++++ .../src/project_panel_settings.rs | 45 + crates/workspace2/src/workspace2.rs | 175 +- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 2 +- crates/zed2/src/zed2.rs | 60 +- 10 files changed, 3140 insertions(+), 171 deletions(-) create mode 100644 crates/project_panel2/Cargo.toml create mode 100644 crates/project_panel2/src/file_associations.rs create mode 100644 crates/project_panel2/src/project_panel.rs create mode 100644 crates/project_panel2/src/project_panel_settings.rs diff --git a/Cargo.lock b/Cargo.lock index 8e2d8d7ace..01bfb08b0a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6587,6 +6587,36 @@ dependencies = [ "workspace", ] +[[package]] +name = "project_panel2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "collections", + "context_menu", + "db2", + "editor2", + "futures 0.3.28", + "gpui2", + "language2", + "menu2", + "postage", + "pretty_assertions", + "project2", + "schemars", + "serde", + "serde_derive", + "serde_json", + "settings2", + "smallvec", + "theme2", + "ui2", + "unicase", + "util", + "workspace2", +] + [[package]] name = "project_symbols" version = "0.1.0" @@ -11394,6 +11424,7 @@ dependencies = [ "parking_lot 0.11.2", "postage", "project2", + "project_panel2", "rand 0.8.5", "regex", "rope2", diff --git a/Cargo.toml b/Cargo.toml index 1b8081d066..4dd2737e5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,6 +79,7 @@ members = [ "crates/project", "crates/project2", "crates/project_panel", + "crates/project_panel2", "crates/project_symbols", "crates/recent_projects", "crates/rope", diff --git a/crates/project_panel2/Cargo.toml b/crates/project_panel2/Cargo.toml new file mode 100644 index 0000000000..bd6bc59a65 --- /dev/null +++ b/crates/project_panel2/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "project_panel2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/project_panel.rs" +doctest = false + +[dependencies] +context_menu = { path = "../context_menu" } +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" } +settings = { path = "../settings2", package = "settings2" } +theme = { path = "../theme2", package = "theme2" } +ui = { path = "../ui2", package = "ui2" } +util = { path = "../util" } +workspace = { path = "../workspace2", package = "workspace2" } +anyhow.workspace = true +postage.workspace = true +futures.workspace = true +serde.workspace = true +serde_derive.workspace = true +serde_json.workspace = true +schemars.workspace = true +smallvec.workspace = true +pretty_assertions.workspace = true +unicase = "2.6" + +[dev-dependencies] +client = { path = "../client2", package = "client2", features = ["test-support"] } +language = { path = "../language2", package = "language2", features = ["test-support"] } +editor = { path = "../editor2", package = "editor2", features = ["test-support"] } +gpui = { path = "../gpui2", package = "gpui2", features = ["test-support"] } +workspace = { path = "../workspace2", package = "workspace2", features = ["test-support"] } +serde_json.workspace = true diff --git a/crates/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs new file mode 100644 index 0000000000..9e9a865f3e --- /dev/null +++ b/crates/project_panel2/src/file_associations.rs @@ -0,0 +1,96 @@ +use std::{path::Path, str, sync::Arc}; + +use collections::HashMap; + +use gpui::{AppContext, AssetSource}; +use serde_derive::Deserialize; +use util::{maybe, paths::PathExt}; + +#[derive(Deserialize, Debug)] +struct TypeConfig { + icon: Arc, +} + +#[derive(Deserialize, Debug)] +pub struct FileAssociations { + suffixes: HashMap, + types: HashMap, +} + +const COLLAPSED_DIRECTORY_TYPE: &'static str = "collapsed_folder"; +const EXPANDED_DIRECTORY_TYPE: &'static str = "expanded_folder"; +const COLLAPSED_CHEVRON_TYPE: &'static str = "collapsed_chevron"; +const EXPANDED_CHEVRON_TYPE: &'static str = "expanded_chevron"; +pub const FILE_TYPES_ASSET: &'static str = "icons/file_icons/file_types.json"; + +pub fn init(assets: impl AssetSource, cx: &mut AppContext) { + cx.set_global(FileAssociations::new(assets)) +} + +impl FileAssociations { + pub fn new(assets: impl AssetSource) -> Self { + assets + .load("icons/file_icons/file_types.json") + .and_then(|file| { + serde_json::from_str::(str::from_utf8(&file).unwrap()) + .map_err(Into::into) + }) + .unwrap_or_else(|_| FileAssociations { + suffixes: HashMap::default(), + types: HashMap::default(), + }) + } + + pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations + maybe!({ + let suffix = path.icon_suffix()?; + + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) + }) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + COLLAPSED_DIRECTORY_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } + + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { + maybe!({ + let this = cx.has_global::().then(|| cx.global::())?; + + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; + + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) + }) + .unwrap_or_else(|| Arc::from("".to_string())) + } +} diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs new file mode 100644 index 0000000000..0844f5190c --- /dev/null +++ b/crates/project_panel2/src/project_panel.rs @@ -0,0 +1,2858 @@ +pub mod file_associations; +mod project_panel_settings; +use settings::Settings; + +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, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, + AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Model, + ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, + StatefulInteractivity, Styled, 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 smallvec::SmallVec; +use std::{ + cmp::Ordering, + collections::{hash_map, HashMap}, + ffi::OsStr, + ops::Range, + path::Path, + sync::Arc, +}; +use theme::ActiveTheme as _; +use ui::{h_stack, v_stack}; +use unicase::UniCase; +use util::TryFutureExt; +use workspace::{ + dock::{DockPosition, PanelEvent}, + Workspace, +}; + +const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; +const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; + +pub struct ProjectPanel { + project: Model, + fs: Arc, + list: UniformListScrollHandle, + focus_handle: FocusHandle, + visible_entries: Vec<(WorktreeId, Vec)>, + last_worktree_root_id: Option, + expanded_dir_ids: HashMap>, + selection: Option, + edit_state: Option, + filename_editor: View, + clipboard_entry: Option, + dragged_entry_destination: Option>, + workspace: WeakView, + has_focus: bool, + width: Option, + pending_serialization: Task>, +} + +#[derive(Copy, Clone, Debug)] +struct Selection { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, +} + +#[derive(Clone, Debug)] +struct EditState { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + is_new_entry: bool, + is_dir: bool, + processing_filename: Option, +} + +#[derive(Copy, Clone)] +pub enum ClipboardEntry { + Copied { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, + Cut { + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + }, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct EntryDetails { + filename: String, + icon: Option>, + path: Arc, + depth: usize, + kind: EntryKind, + is_ignored: bool, + is_expanded: bool, + is_selected: bool, + is_editing: bool, + is_processing: bool, + is_cut: bool, + git_status: Option, +} + +actions!( + 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.add_action(ProjectPanel::expand_selected_entry); + // cx.add_action(ProjectPanel::collapse_selected_entry); + // cx.add_action(ProjectPanel::collapse_all_entries); + // cx.add_action(ProjectPanel::select_prev); + // cx.add_action(ProjectPanel::select_next); + // cx.add_action(ProjectPanel::new_file); + // cx.add_action(ProjectPanel::new_directory); + // cx.add_action(ProjectPanel::rename); + // cx.add_async_action(ProjectPanel::delete); + // cx.add_async_action(ProjectPanel::confirm); + // cx.add_async_action(ProjectPanel::open_file); + // cx.add_action(ProjectPanel::cancel); + // cx.add_action(ProjectPanel::cut); + // cx.add_action(ProjectPanel::copy); + // cx.add_action(ProjectPanel::copy_path); + // cx.add_action(ProjectPanel::copy_relative_path); + // cx.add_action(ProjectPanel::reveal_in_finder); + // cx.add_action(ProjectPanel::open_in_terminal); + // cx.add_action(ProjectPanel::new_search_in_directory); + // cx.add_action( + // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { + // this.paste(action, cx); + // }, + // ); +} + +#[derive(Debug)] +pub enum Event { + OpenedEntry { + entry_id: ProjectEntryId, + focus_opened_item: bool, + }, + SplitEntry { + entry_id: ProjectEntryId, + }, + DockPositionChanged, + Focus, + NewSearchInDirectory { + dir_entry: Entry, + }, + ActivatePanel, +} + +#[derive(Serialize, Deserialize)] +struct SerializedProjectPanel { + width: Option, +} + +impl ProjectPanel { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + let project = workspace.project().clone(); + let project_panel = cx.build_view(|cx: &mut ViewContext| { + cx.observe(&project, |this, _, cx| { + this.update_visible_entries(None, cx); + cx.notify(); + }) + .detach(); + let focus_handle = cx.focus_handle(); + + cx.on_focus(&focus_handle, Self::focus_in).detach(); + cx.on_blur(&focus_handle, Self::focus_out).detach(); + + cx.subscribe(&project, |this, project, event, cx| match event { + project::Event::ActiveEntryChanged(Some(entry_id)) => { + if let Some(worktree_id) = project.read(cx).worktree_id_for_entry(*entry_id, cx) + { + this.expand_entry(worktree_id, *entry_id, cx); + this.update_visible_entries(Some((worktree_id, *entry_id)), cx); + this.autoscroll(cx); + cx.notify(); + } + } + project::Event::ActivateProjectPanel => { + cx.emit(Event::ActivatePanel); + } + project::Event::WorktreeRemoved(id) => { + this.expanded_dir_ids.remove(id); + this.update_visible_entries(None, cx); + cx.notify(); + } + _ => {} + }) + .detach(); + + let filename_editor = cx.build_view(|cx| Editor::single_line(cx)); + + cx.subscribe(&filename_editor, |this, _, event, cx| match event { + editor::Event::BufferEdited | editor::Event::SelectionsChanged { .. } => { + this.autoscroll(cx); + } + _ => {} + }) + .detach(); + + // cx.observe_focus(&filename_editor, |this, _, is_focused, cx| { + // if !is_focused + // && this + // .edit_state + // .as_ref() + // .map_or(false, |state| state.processing_filename.is_none()) + // { + // this.edit_state = None; + // this.update_visible_entries(None, cx); + // } + // }) + // .detach(); + + // cx.observe_global::(|_, cx| { + // cx.notify(); + // }) + // .detach(); + + let view_id = cx.view().entity_id(); + 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, + filename_editor, + clipboard_entry: None, + // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + dragged_entry_destination: None, + workspace: workspace.weak_handle(), + has_focus: false, + width: None, + pending_serialization: Task::ready(None), + }; + this.update_visible_entries(None, cx); + + // Update the dock position when the setting changes. + // todo!() + // let mut old_dock_position = this.position(cx); + // cx.observe_global::(move |this, cx| { + // let new_dock_position = this.position(cx); + // if new_dock_position != old_dock_position { + // old_dock_position = new_dock_position; + // cx.emit(Event::DockPositionChanged); + // } + // }) + // .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 fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // let serialized_panel = if let Some(panel) = cx + // .background_executor() + // .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) + // .await + // .log_err() + // .flatten() + // { + // Some(serde_json::from_str::(&panel)?) + // } else { + // None + // }; + workspace.update(&mut cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + // if let Some(serialized_panel) = serialized_panel { + // panel.update(cx, |panel, cx| { + // panel.width = serialized_panel.width; + // cx.notify(); + // }); + // } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + self.pending_serialization = cx.background_executor().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + PROJECT_PANEL_KEY.into(), + serde_json::to_string(&SerializedProjectPanel { width })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + + fn focus_in(&mut self, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: &mut ViewContext) { + self.has_focus = false; + } + + fn deploy_context_menu( + &mut self, + position: Point, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + // 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, + // }); + + // let mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } + // } + // menu_entries.push(ContextMenuItem::action("New File", NewFile)); + // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } + // } + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); + // menu_entries.push(ContextMenuItem::action( + // "Copy Relative Path", + // CopyRelativePath, + // )); + + // 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_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } + // } + + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); + + // cx.notify(); + } + + fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + let worktree_id = worktree.id(); + let entry_id = entry.id; + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + match expanded_dir_ids.binary_search(&entry_id) { + Ok(_) => self.select_next(&SelectNext, cx), + Err(ix) => { + self.project.update(cx, |project, cx| { + project.expand_entry(worktree_id, entry_id, cx); + }); + + expanded_dir_ids.insert(ix, entry_id); + self.update_visible_entries(None, cx); + cx.notify(); + } + } + } + } + } + + fn collapse_selected_entry(&mut self, _: &CollapseSelectedEntry, cx: &mut ViewContext) { + if let Some((worktree, mut entry)) = self.selected_entry(cx) { + let worktree_id = worktree.id(); + let expanded_dir_ids = + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + expanded_dir_ids + } else { + return; + }; + + loop { + let entry_id = entry.id; + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.notify(); + break; + } + Err(_) => { + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + } + } + + pub fn collapse_all_entries(&mut self, _: &CollapseAllEntries, cx: &mut ViewContext) { + self.expanded_dir_ids.clear(); + self.update_visible_entries(None, cx); + cx.notify(); + } + + fn toggle_expanded(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + if let Some(worktree_id) = self.project.read(cx).worktree_id_for_entry(entry_id, cx) { + if let Some(expanded_dir_ids) = self.expanded_dir_ids.get_mut(&worktree_id) { + self.project.update(cx, |project, cx| { + match expanded_dir_ids.binary_search(&entry_id) { + Ok(ix) => { + expanded_dir_ids.remove(ix); + } + Err(ix) => { + project.expand_entry(worktree_id, entry_id, cx); + expanded_dir_ids.insert(ix, entry_id); + } + } + }); + self.update_visible_entries(Some((worktree_id, entry_id)), cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + } + } + + fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if entry_ix > 0 { + entry_ix -= 1; + } else if worktree_ix > 0 { + worktree_ix -= 1; + entry_ix = self.visible_entries[worktree_ix].1.len() - 1; + } else { + return; + } + + let (worktree_id, worktree_entries) = &self.visible_entries[worktree_ix]; + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: worktree_entries[entry_ix].id, + }); + self.autoscroll(cx); + cx.notify(); + } else { + self.select_first(cx); + } + } + + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + if let Some(task) = self.confirm_edit(cx) { + return Some(task); + } + + None + } + + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_file() { + self.open_entry(entry.id, true, cx); + } + } + + None + } + + fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { + let edit_state = self.edit_state.as_mut()?; + cx.focus(&self.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(); + })?; + + let new_entry = new_entry?; + this.update(&mut cx, |this, cx| { + if let Some(selection) = &mut this.selection { + if selection.entry_id == edited_entry_id { + selection.worktree_id = worktree_id; + selection.entry_id = new_entry.id; + this.expand_to_selection(cx); + } + } + this.update_visible_entries(None, cx); + if is_new_entry && !is_dir { + this.open_entry(new_entry.id, true, cx); + } + cx.notify(); + })?; + Ok(()) + })) + } + + fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + self.edit_state = None; + self.update_visible_entries(None, cx); + cx.focus(&self.focus_handle); + cx.notify(); + } + + fn open_entry( + &mut self, + entry_id: ProjectEntryId, + focus_opened_item: bool, + cx: &mut ViewContext, + ) { + cx.emit(Event::OpenedEntry { + entry_id, + focus_opened_item, + }); + } + + fn split_entry(&mut self, entry_id: ProjectEntryId, cx: &mut ViewContext) { + cx.emit(Event::SplitEntry { entry_id }); + } + + fn new_file(&mut self, _: &NewFile, cx: &mut ViewContext) { + self.add_entry(false, cx) + } + + fn new_directory(&mut self, _: &NewDirectory, cx: &mut ViewContext) { + self.add_entry(true, cx) + } + + fn add_entry(&mut self, is_dir: bool, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + let directory_id; + if let Some((worktree, expanded_dir_ids)) = self + .project + .read(cx) + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + let worktree = worktree.read(cx); + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if entry.is_dir() { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + directory_id = entry.id; + break; + } else { + if let Some(parent_path) = entry.path.parent() { + if let Some(parent_entry) = worktree.entry_for_path(parent_path) { + entry = parent_entry; + continue; + } + } + return; + } + } + } else { + return; + }; + } else { + return; + }; + + self.edit_state = Some(EditState { + worktree_id, + entry_id: directory_id, + is_new_entry: true, + is_dir, + processing_filename: None, + }); + self.filename_editor.update(cx, |editor, cx| { + editor.clear(cx); + editor.focus(cx); + }); + self.update_visible_entries(Some((worktree_id, NEW_ENTRY_ID)), cx); + self.autoscroll(cx); + cx.notify(); + } + } + + fn rename(&mut self, _: &Rename, cx: &mut ViewContext) { + if let Some(Selection { + worktree_id, + entry_id, + }) = self.selection + { + if let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) { + if let Some(entry) = worktree.read(cx).entry_for_id(entry_id) { + self.edit_state = Some(EditState { + worktree_id, + entry_id, + is_new_entry: false, + is_dir: entry.is_dir(), + processing_filename: None, + }); + let file_name = entry + .path + .file_name() + .map(|s| s.to_string_lossy()) + .unwrap_or_default() + .to_string(); + let file_stem = entry.path.file_stem().map(|s| s.to_string_lossy()); + let selection_end = + file_stem.map_or(file_name.len(), |file_stem| file_stem.len()); + self.filename_editor.update(cx, |editor, cx| { + editor.set_text(file_name, cx); + editor.change_selections(Some(Autoscroll::fit()), cx, |s| { + s.select_ranges([0..selection_end]) + }); + editor.focus(cx); + }); + self.update_visible_entries(None, cx); + self.autoscroll(cx); + cx.notify(); + } + } + + // cx.update_global(|drag_and_drop: &mut DragAndDrop, cx| { + // drag_and_drop.cancel_dragging::(cx); + // }) + } + } + + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; + + let mut answer = cx.prompt( + PromptLevel::Info, + &format!("Delete {file_name:?}?"), + &["Delete", "Cancel"], + ); + Some(cx.spawn(|this, mut cx| async move { + if answer.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 + })) + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(selection) = self.selection { + let (mut worktree_ix, mut entry_ix, _) = + self.index_for_selection(selection).unwrap_or_default(); + if let Some((_, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if entry_ix + 1 < worktree_entries.len() { + entry_ix += 1; + } else { + worktree_ix += 1; + entry_ix = 0; + } + } + + if let Some((worktree_id, worktree_entries)) = self.visible_entries.get(worktree_ix) { + if let Some(entry) = worktree_entries.get(entry_ix) { + self.selection = Some(Selection { + worktree_id: *worktree_id, + entry_id: entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } else { + self.select_first(cx); + } + } + + fn select_first(&mut self, cx: &mut ViewContext) { + let worktree = self + .visible_entries + .first() + .and_then(|(worktree_id, _)| self.project.read(cx).worktree_for_id(*worktree_id, cx)); + if let Some(worktree) = worktree { + let worktree = worktree.read(cx); + let worktree_id = worktree.id(); + if let Some(root_entry) = worktree.root_entry() { + self.selection = Some(Selection { + worktree_id, + entry_id: root_entry.id, + }); + self.autoscroll(cx); + cx.notify(); + } + } + } + + fn autoscroll(&mut self, cx: &mut ViewContext) { + if let Some((_, _, index)) = self.selection.and_then(|s| self.index_for_selection(s)) { + self.list.scroll_to_item(index); + cx.notify(); + } + } + + fn cut(&mut self, _: &Cut, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Cut { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn copy(&mut self, _: &Copy, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + self.clipboard_entry = Some(ClipboardEntry::Copied { + worktree_id: worktree.id(), + entry_id: entry.id, + }); + cx.notify(); + } + } + + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { + if let Some((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() { + if let Some(task) = self.project.update(cx, |project, cx| { + project.rename_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } else if let Some(task) = self.project.update(cx, |project, cx| { + project.copy_entry(clipboard_entry.entry_id(), new_path, cx) + }) { + task.detach_and_log_err(cx) + } + } + None + } + + fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new( + worktree + .abs_path() + .join(&entry.path) + .to_string_lossy() + .to_string(), + )); + } + } + + fn copy_relative_path(&mut self, _: &CopyRelativePath, cx: &mut ViewContext) { + if let Some((_, entry)) = self.selected_entry(cx) { + cx.write_to_clipboard(ClipboardItem::new(entry.path.to_string_lossy().to_string())); + } + } + + fn reveal_in_finder(&mut self, _: &RevealInFinder, cx: &mut ViewContext) { + if let Some((worktree, entry)) = self.selected_entry(cx) { + cx.reveal_path(&worktree.abs_path().join(&entry.path)); + } + } + + fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + todo!() + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let window = cx.window(); + // let view_id = cx.view_id(); + // let path = worktree.abs_path().join(&entry.path); + + // cx.app_context() + // .spawn(|mut cx| async move { + // window.dispatch_action( + // view_id, + // &workspace::OpenTerminal { + // working_directory: path, + // }, + // &mut cx, + // ); + // }) + // .detach(); + // } + } + + pub fn new_search_in_directory( + &mut self, + _: &NewSearchInDirectory, + cx: &mut ViewContext, + ) { + if let Some((_, entry)) = self.selected_entry(cx) { + if entry.is_dir() { + cx.emit(Event::NewSearchInDirectory { + dir_entry: entry.clone(), + }); + } + } + } + + fn move_entry( + &mut self, + entry_to_move: ProjectEntryId, + destination: ProjectEntryId, + destination_is_file: bool, + cx: &mut ViewContext, + ) { + let destination_worktree = self.project.update(cx, |project, cx| { + let entry_path = project.path_for_entry(entry_to_move, cx)?; + let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + + let mut destination_path = destination_entry_path.as_ref(); + if destination_is_file { + destination_path = destination_path.parent()?; + } + + let mut new_path = destination_path.to_path_buf(); + new_path.push(entry_path.path.file_name()?); + if new_path != entry_path.path.as_ref() { + let task = project.rename_entry(entry_to_move, new_path, cx)?; + cx.foreground_executor().spawn(task).detach_and_log_err(cx); + } + + Some(project.worktree_id_for_entry(destination, cx)?) + }); + + if let Some(destination_worktree) = destination_worktree { + self.expand_entry(destination_worktree, destination, cx); + } + } + + fn index_for_selection(&self, selection: Selection) -> Option<(usize, usize, usize)> { + let mut entry_index = 0; + let mut visible_entries_index = 0; + for (worktree_index, (worktree_id, worktree_entries)) in + self.visible_entries.iter().enumerate() + { + if *worktree_id == selection.worktree_id { + for entry in worktree_entries { + if entry.id == selection.entry_id { + return Some((worktree_index, entry_index, visible_entries_index)); + } else { + visible_entries_index += 1; + entry_index += 1; + } + } + break; + } else { + visible_entries_index += worktree_entries.len(); + } + } + None + } + + pub fn selected_entry<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(&'a Worktree, &'a project::Entry)> { + let (worktree, entry) = self.selected_entry_handle(cx)?; + Some((worktree.read(cx), entry)) + } + + fn selected_entry_handle<'a>( + &self, + cx: &'a AppContext, + ) -> Option<(Model, &'a project::Entry)> { + let selection = self.selection?; + let project = self.project.read(cx); + let worktree = project.worktree_for_id(selection.worktree_id, cx)?; + let entry = worktree.read(cx).entry_for_id(selection.entry_id)?; + Some((worktree, entry)) + } + + fn expand_to_selection(&mut self, cx: &mut ViewContext) -> Option<()> { + let (worktree, entry) = self.selected_entry(cx)?; + let expanded_dir_ids = self.expanded_dir_ids.entry(worktree.id()).or_default(); + + for path in entry.path.ancestors() { + let Some(entry) = worktree.entry_for_path(path) else { + continue; + }; + if entry.is_dir() { + if let Err(idx) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(idx, entry.id); + } + } + } + + Some(()) + } + + fn update_visible_entries( + &mut self, + new_selected_entry: Option<(WorktreeId, ProjectEntryId)>, + cx: &mut ViewContext, + ) { + let project = self.project.read(cx); + self.last_worktree_root_id = project + .visible_worktrees(cx) + .rev() + .next() + .and_then(|worktree| worktree.read(cx).root_entry()) + .map(|entry| entry.id); + + self.visible_entries.clear(); + for worktree in project.visible_worktrees(cx) { + let snapshot = worktree.read(cx).snapshot(); + let worktree_id = snapshot.id(); + + let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) { + hash_map::Entry::Occupied(e) => e.into_mut(), + hash_map::Entry::Vacant(e) => { + // The first time a worktree's root entry becomes available, + // mark that root entry as expanded. + if let Some(entry) = snapshot.root_entry() { + e.insert(vec![entry.id]).as_slice() + } else { + &[] + } + } + }; + + let mut new_entry_parent_id = None; + let mut new_entry_kind = EntryKind::Dir; + if let Some(edit_state) = &self.edit_state { + if edit_state.worktree_id == worktree_id && edit_state.is_new_entry { + new_entry_parent_id = Some(edit_state.entry_id); + new_entry_kind = if edit_state.is_dir { + EntryKind::Dir + } else { + EntryKind::File(Default::default()) + }; + } + } + + let mut visible_worktree_entries = Vec::new(); + let mut entry_iter = snapshot.entries(true); + + while let Some(entry) = entry_iter.entry() { + visible_worktree_entries.push(entry.clone()); + if Some(entry.id) == new_entry_parent_id { + visible_worktree_entries.push(Entry { + id: NEW_ENTRY_ID, + kind: new_entry_kind, + path: entry.path.join("\0").into(), + inode: 0, + mtime: entry.mtime, + is_symlink: false, + is_ignored: false, + is_external: false, + git_status: entry.git_status, + }); + } + if expanded_dir_ids.binary_search(&entry.id).is_err() + && entry_iter.advance_to_sibling() + { + continue; + } + entry_iter.advance(); + } + + snapshot.propagate_git_statuses(&mut visible_worktree_entries); + + visible_worktree_entries.sort_by(|entry_a, entry_b| { + let mut components_a = entry_a.path.components().peekable(); + let mut components_b = entry_b.path.components().peekable(); + loop { + match (components_a.next(), components_b.next()) { + (Some(component_a), Some(component_b)) => { + let a_is_file = components_a.peek().is_none() && entry_a.is_file(); + let b_is_file = components_b.peek().is_none() && entry_b.is_file(); + let ordering = a_is_file.cmp(&b_is_file).then_with(|| { + let name_a = + UniCase::new(component_a.as_os_str().to_string_lossy()); + let name_b = + UniCase::new(component_b.as_os_str().to_string_lossy()); + name_a.cmp(&name_b) + }); + if !ordering.is_eq() { + return ordering; + } + } + (Some(_), None) => break Ordering::Greater, + (None, Some(_)) => break Ordering::Less, + (None, None) => break Ordering::Equal, + } + } + }); + self.visible_entries + .push((worktree_id, visible_worktree_entries)); + } + + if let Some((worktree_id, entry_id)) = new_selected_entry { + self.selection = Some(Selection { + worktree_id, + entry_id, + }); + } + } + + fn expand_entry( + &mut self, + worktree_id: WorktreeId, + entry_id: ProjectEntryId, + cx: &mut ViewContext, + ) { + self.project.update(cx, |project, cx| { + if let Some((worktree, expanded_dir_ids)) = project + .worktree_for_id(worktree_id, cx) + .zip(self.expanded_dir_ids.get_mut(&worktree_id)) + { + project.expand_entry(worktree_id, entry_id, cx); + let worktree = worktree.read(cx); + + if let Some(mut entry) = worktree.entry_for_id(entry_id) { + loop { + if let Err(ix) = expanded_dir_ids.binary_search(&entry.id) { + expanded_dir_ids.insert(ix, entry.id); + } + + if let Some(parent_entry) = + entry.path.parent().and_then(|p| worktree.entry_for_path(p)) + { + entry = parent_entry; + } else { + break; + } + } + } + } + }); + } + + fn for_each_visible_entry( + &self, + range: Range, + cx: &mut ViewContext, + mut callback: impl FnMut(ProjectEntryId, EntryDetails, &mut ViewContext), + ) { + let mut ix = 0; + for (worktree_id, visible_worktree_entries) in &self.visible_entries { + if ix >= range.end { + return; + } + + if ix + visible_worktree_entries.len() <= range.start { + ix += visible_worktree_entries.len(); + continue; + } + + let end_ix = range.end.min(ix + visible_worktree_entries.len()); + let (git_status_setting, show_file_icons, show_folder_icons) = { + let settings = ProjectPanelSettings::get_global(cx); + ( + settings.git_status, + settings.file_icons, + settings.folder_icons, + ) + }; + if let Some(worktree) = self.project.read(cx).worktree_for_id(*worktree_id, cx) { + let snapshot = worktree.read(cx).snapshot(); + let root_name = OsStr::new(snapshot.root_name()); + let expanded_entry_ids = self + .expanded_dir_ids + .get(&snapshot.id()) + .map(Vec::as_slice) + .unwrap_or(&[]); + + let entry_range = range.start.saturating_sub(ix)..end_ix - ix; + for entry in visible_worktree_entries[entry_range].iter() { + let status = git_status_setting.then(|| entry.git_status).flatten(); + let is_expanded = expanded_entry_ids.binary_search(&entry.id).is_ok(); + let icon = match entry.kind { + EntryKind::File(_) => { + if show_file_icons { + Some(FileAssociations::get_icon(&entry.path, cx)) + } else { + None + } + } + _ => { + if show_folder_icons { + Some(FileAssociations::get_folder_icon(is_expanded, cx)) + } else { + Some(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_visual_element( + details: &EntryDetails, + editor: Option<&View>, + padding: Pixels, + cx: &mut ViewContext, + ) -> Div { + let show_editor = details.is_editing && !details.is_processing; + + let theme = cx.theme(); + let filename_text_color = details + .git_status + .as_ref() + .map(|status| match status { + GitFileStatus::Added => theme.styles.status.created, + GitFileStatus::Modified => theme.styles.status.modified, + GitFileStatus::Conflict => theme.styles.status.conflict, + }) + .unwrap_or(theme.styles.status.info); + + h_stack() + .child(if let Some(icon) = &details.icon { + div().child(svg().path(icon.to_string())) + } else { + div() + }) + .child( + if let (Some(editor), true) = (editor, show_editor) { + div().child(editor.clone()) + } else { + div().child(details.filename.clone()) + } + .ml_1(), + ) + .pl(padding) + } + + fn render_entry( + entry_id: ProjectEntryId, + details: EntryDetails, + editor: &View, + // dragged_entry_destination: &mut Option>, + // theme: &theme::ProjectPanel, + cx: &mut ViewContext, + ) -> Div> { + let kind = details.kind; + let settings = ProjectPanelSettings::get_global(cx); + const INDENT_SIZE: Pixels = px(16.0); + let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); + let show_editor = details.is_editing && !details.is_processing; + + Self::render_entry_visual_element(&details, Some(editor), padding, cx) + .id(entry_id.to_proto() as usize) + .on_click(move |this, event, cx| { + 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_down(MouseButton::Right, move |event, this, cx| { + // this.deploy_context_menu(event.position, entry_id, cx); + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_entry)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.move_entry( + // *dragged_entry, + // entry_id, + // matches!(details.kind, EntryKind::File(_)), + // cx, + // ); + // } + // }) + } +} + +impl Render for ProjectPanel { + type Element = Div>; + + fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { + enum ProjectPanel {} + let theme = cx.theme(); + let last_worktree_root_id = self.last_worktree_root_id; + + let has_worktree = self.visible_entries.len() != 0; + + if has_worktree { + div().id("project-panel").child( + uniform_list( + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + |this: &mut Self, range, cx| { + let mut items = SmallVec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + // &mut dragged_entry_destination, + cx, + )); + }); + items + }, + ) + .track_scroll(self.list.clone()), + ) + } else { + v_stack().id("empty-project_panel") + } + } +} + +impl EventEmitter for ProjectPanel {} + +impl EventEmitter for ProjectPanel {} + +impl workspace::dock::Panel for ProjectPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match ProjectPanelSettings::get_global(cx).dock { + ProjectPanelDockPosition::Left => DockPosition::Left, + ProjectPanelDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, + DockPosition::Right => ProjectPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.width + .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + self.serialize(cx); + cx.notify(); + } + + fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { + Some("icons/project.svg") + } + + fn icon_tooltip(&self) -> (String, Option>) { + ("Project Panel".into(), Some(Box::new(ToggleFocus))) + } + + // fn should_change_position_on_event(event: &Self::Event) -> bool { + // matches!(event, Event::DockPositionChanged) + // } + + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus + } + + fn persistent_name(&self) -> &'static str { + "Project Panel" + } + + // fn is_focus_event(event: &Self::Event) -> bool { + // matches!(event, Event::Focus) + // } +} + +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, + } + } +} + +// todo!() +// #[cfg(test)] +// mod tests { +// use super::*; +// use gpui::{AnyWindowHandle, TestAppContext, View, WindowHandle}; +// use pretty_assertions::assert_eq; +// use project::FakeFs; +// use serde_json::json; +// use settings::SettingsStore; +// use std::{ +// collections::HashSet, +// path::{Path, PathBuf}, +// sync::atomic::{self, AtomicUsize}, +// }; +// use workspace::{pane, 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)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); +// 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(iterations = 30)] +// async fn test_editing_files(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// 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)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.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(&Confirm, 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(&Confirm, cx).unwrap() +// }) +// .await +// .unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " another-filename.txt <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// select_path(&panel, "root1/b/another-filename.txt", cx); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'another-filename.txt'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "another-filename".len(), "Should not select file extension"); + +// editor.set_text("a-different-filename.tar.gz", cx) +// }); +// panel.confirm(&Confirm, cx).unwrap() +// }); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [PROCESSING: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// confirm.await.unwrap(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > 3", +// " > 4", +// " [EDITOR: 'a-different-filename.tar.gz'] <== selected", +// " > C", +// " .dockerignore", +// " the-new-filename", +// ] +// ); + +// panel.update(cx, |panel, cx| { +// panel.filename_editor.update(cx, |editor, cx| { +// let file_name_selections = editor.selections.all::(cx); +// assert_eq!(file_name_selections.len(), 1, "File editing should have a single selection, but got: {file_name_selections:?}"); +// let file_name_selection = &file_name_selections[0]; +// assert_eq!(file_name_selection.start, 0, "Should select the file name from the start"); +// assert_eq!(file_name_selection.end, "a-different-filename.tar".len(), "Should not select file extension, but still may select anything up to the last dot"); + +// }); +// panel.cancel(&Cancel, cx) +// }); + +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v root1", +// " > .git", +// " > a", +// " v b", +// " > [EDITOR: ''] <== selected", +// " > 3", +// " > 4", +// " a-different-filename.tar.gz", +// " > C", +// " .dockerignore", +// ] +// ); + +// let confirm = panel.update(cx, |panel, cx| { +// panel +// .filename_editor +// .update(cx, |editor, cx| editor.set_text("new-dir", cx)); +// panel.confirm(&Confirm, 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.focus_self()); +// 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 = 30)] +// async fn test_adding_directories_via_file(cx: &mut gpui::TestAppContext) { +// init_test(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/root1", +// json!({ +// ".dockerignore": "", +// ".git": { +// "HEAD": "", +// }, +// "a": { +// "0": { "q": "", "r": "", "s": "" }, +// "1": { "t": "", "u": "" }, +// "2": { "v": "", "w": "", "x": "", "y": "" }, +// }, +// "b": { +// "3": { "Q": "" }, +// "4": { "R": "", "S": "", "T": "", "U": "" }, +// }, +// "C": { +// "5": {}, +// "6": { "V": "", "W": "" }, +// "7": { "X": "" }, +// "8": { "Y": {}, "Z": "" } +// } +// }), +// ) +// .await; +// fs.insert_tree( +// "/root2", +// json!({ +// "d": { +// "9": "" +// }, +// "e": {} +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// 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)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.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(&Confirm, 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.background()); +// 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)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// 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.foreground().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.foreground().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.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// 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(window, "test/first.rs", cx); + +// submit_deletion(window.into(), &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(window.into(), &workspace, cx); + +// select_path(&panel, "src/test/second.rs", cx); +// panel.update(cx, |panel, cx| panel.open_file(&Open, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " second.rs <== selected", +// " third.rs" +// ] +// ); +// ensure_single_file_is_opened(window, "test/second.rs", cx); + +// window.update(cx, |cx| { +// let active_items = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()) +// .collect::>(); +// assert_eq!(active_items.len(), 1); +// let open_editor = active_items +// .into_iter() +// .next() +// .unwrap() +// .downcast::() +// .expect("Open item should be an editor"); +// open_editor.update(cx, |editor, cx| editor.set_text("Another text!", cx)); +// }); +// submit_deletion(window.into(), &panel, cx); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src", " v test", " third.rs"], +// "Project panel should have no deleted file, with one last file remaining" +// ); +// ensure_no_open_items_and_panes(window.into(), &workspace, cx); +// } + +// #[gpui::test] +// async fn test_create_duplicate_items(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let window = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); +// let workspace = window.root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// select_path(&panel, "src/", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &["v src <== selected", " > test"] +// ); +// panel.update(cx, |panel, cx| panel.new_directory(&NewDirectory, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.is_focused(cx)); +// }); +// 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(&Confirm, 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.foreground().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)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.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(&Confirm, 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.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ], +// ); +// panel.update(cx, |panel, cx| panel.rename(&Rename, cx)); +// window.read_with(cx, |cx| { +// let panel = panel.read(cx); +// assert!(panel.filename_editor.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(&Confirm, 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_new_search_in_directory_trigger(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// fs.insert_tree( +// "/src", +// json!({ +// "test": { +// "first.rs": "// First Rust file", +// "second.rs": "// Second Rust file", +// "third.rs": "// Third Rust file", +// } +// }), +// ) +// .await; + +// let project = Project::test(fs.clone(), ["/src".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// let new_search_events_count = Arc::new(AtomicUsize::new(0)); +// let _subscription = panel.update(cx, |_, cx| { +// let subcription_count = Arc::clone(&new_search_events_count); +// cx.subscribe(&cx.handle(), move |_, _, event, _| { +// if matches!(event, Event::NewSearchInDirectory { .. }) { +// subcription_count.fetch_add(1, atomic::Ordering::SeqCst); +// } +// }) +// }); + +// toggle_expand_dir(&panel, "src/test", cx); +// select_path(&panel, "src/test/first.rs", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test", +// " first.rs <== selected", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 0, +// "Should not trigger new search in directory when called on a file" +// ); + +// select_path(&panel, "src/test", cx); +// panel.update(cx, |panel, cx| panel.confirm(&Confirm, cx)); +// cx.foreground().run_until_parked(); +// assert_eq!( +// visible_entries_as_strings(&panel, 0..10, cx), +// &[ +// "v src", +// " v test <== selected", +// " first.rs", +// " second.rs", +// " third.rs" +// ] +// ); +// panel.update(cx, |panel, cx| { +// panel.new_search_in_directory(&NewSearchInDirectory, cx) +// }); +// assert_eq!( +// new_search_events_count.load(atomic::Ordering::SeqCst), +// 1, +// "Should trigger new search in directory when called on a directory" +// ); +// } + +// #[gpui::test] +// async fn test_collapse_all_entries(cx: &mut gpui::TestAppContext) { +// init_test_with_editor(cx); + +// let fs = FakeFs::new(cx.background()); +// 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)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// panel.update(cx, |panel, cx| { +// panel.collapse_all_entries(&CollapseAllEntries, cx) +// }); +// cx.foreground().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.foreground().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.background()); +// fs.as_fake().insert_tree("/root", json!({})).await; +// let project = Project::test(fs, ["/root".as_ref()], cx).await; +// let workspace = cx +// .add_window(|cx| Workspace::test_new(project.clone(), cx)) +// .root(cx); +// let panel = workspace.update(cx, |workspace, cx| ProjectPanel::new(workspace, cx)); + +// // Make a new buffer with no backing file +// workspace.update(cx, |workspace, cx| { +// Editor::new_file(workspace, &Default::default(), cx) +// }); + +// // "Save as"" the buffer, creating a new backing file for it +// let task = workspace.update(cx, |workspace, cx| { +// workspace.save_active_item(workspace::SaveIntent::Save, cx) +// }); + +// cx.foreground().run_until_parked(); +// cx.simulate_new_path_selection(|_| Some(PathBuf::from("/root/new"))); +// 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)) +// .unwrap() +// .await +// .unwrap(); + +// cx.foreground().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) +// }) +// .await +// .unwrap(); + +// cx.foreground().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"] +// ); +// } + +// fn toggle_expand_dir( +// panel: &View, +// path: impl AsRef, +// cx: &mut TestAppContext, +// ) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.toggle_expanded(entry_id, cx); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn select_path(panel: &View, path: impl AsRef, cx: &mut TestAppContext) { +// let path = path.as_ref(); +// panel.update(cx, |panel, cx| { +// for worktree in panel.project.read(cx).worktrees().collect::>() { +// let worktree = worktree.read(cx); +// if let Ok(relative_path) = path.strip_prefix(worktree.root_name()) { +// let entry_id = worktree.entry_for_path(relative_path).unwrap().id; +// panel.selection = Some(Selection { +// worktree_id: worktree.id(), +// entry_id, +// }); +// return; +// } +// } +// panic!("no worktree for path {:?}", path); +// }); +// } + +// fn visible_entries_as_strings( +// panel: &View, +// range: Range, +// cx: &mut TestAppContext, +// ) -> Vec { +// let mut result = Vec::new(); +// let mut project_entries = HashSet::new(); +// let mut has_editor = false; + +// panel.update(cx, |panel, cx| { +// panel.for_each_visible_entry(range, cx, |project_entry, details, _| { +// if details.is_editing { +// assert!(!has_editor, "duplicate editor entry"); +// has_editor = true; +// } else { +// assert!( +// project_entries.insert(project_entry), +// "duplicate project entry {:?} {:?}", +// project_entry, +// details +// ); +// } + +// 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.foreground().forbid_parking(); +// cx.update(|cx| { +// cx.set_global(SettingsStore::test(cx)); +// init_settings(cx); +// theme::init(cx); +// language::init(cx); +// editor::init_settings(cx); +// crate::init((), cx); +// workspace::init_settings(cx); +// client::init_settings(cx); +// Project::init_settings(cx); +// }); +// } + +// fn init_test_with_editor(cx: &mut TestAppContext) { +// cx.foreground().forbid_parking(); +// cx.update(|cx| { +// let app_state = AppState::test(cx); +// theme::init(cx); +// init_settings(cx); +// language::init(cx); +// editor::init(cx); +// pane::init(cx); +// crate::init((), cx); +// workspace::init(app_state.clone(), cx); +// Project::init_settings(cx); +// }); +// } + +// fn ensure_single_file_is_opened( +// window: WindowHandle, +// expected_path: &str, +// cx: &mut TestAppContext, +// ) { +// window.update_root(cx, |workspace, cx| { +// let worktrees = workspace.worktrees(cx).collect::>(); +// assert_eq!(worktrees.len(), 1); +// let worktree_id = WorktreeId::from_usize(worktrees[0].id()); + +// let open_project_paths = workspace +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert_eq!( +// open_project_paths, +// vec![ProjectPath { +// worktree_id, +// path: Arc::from(Path::new(expected_path)) +// }], +// "Should have opened file, selected in project panel" +// ); +// }); +// } + +// fn submit_deletion( +// window: AnyWindowHandle, +// panel: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts before the deletion" +// ); +// panel.update(cx, |panel, cx| { +// panel +// .delete(&Delete, cx) +// .expect("Deletion start") +// .detach_and_log_err(cx); +// }); +// assert!( +// window.has_pending_prompt(cx), +// "Should have a prompt after the deletion" +// ); +// window.simulate_prompt_answer(0, cx); +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after prompt was replied to" +// ); +// cx.foreground().run_until_parked(); +// } + +// fn ensure_no_open_items_and_panes( +// window: AnyWindowHandle, +// workspace: &View, +// cx: &mut TestAppContext, +// ) { +// assert!( +// !window.has_pending_prompt(cx), +// "Should have no prompts after deletion operation closes the file" +// ); +// window.read_with(cx, |cx| { +// let open_project_paths = workspace +// .read(cx) +// .panes() +// .iter() +// .filter_map(|pane| pane.read(cx).active_item()?.project_path(cx)) +// .collect::>(); +// assert!( +// open_project_paths.is_empty(), +// "Deleted file's buffer should be closed, but got open files: {open_project_paths:?}" +// ); +// }); +// } +// } diff --git a/crates/project_panel2/src/project_panel_settings.rs b/crates/project_panel2/src/project_panel_settings.rs new file mode 100644 index 0000000000..5b0e0194a5 --- /dev/null +++ b/crates/project_panel2/src/project_panel_settings.rs @@ -0,0 +1,45 @@ +use anyhow; +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: f32, + pub dock: ProjectPanelDockPosition, + pub file_icons: bool, + pub folder_icons: bool, + pub git_status: bool, + pub indent_size: f32, +} + +#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] +pub struct ProjectPanelSettingsContent { + pub default_width: Option, + pub dock: Option, + pub file_icons: Option, + pub folder_icons: Option, + pub git_status: Option, + pub indent_size: Option, +} + +impl Settings for ProjectPanelSettings { + const KEY: Option<&'static str> = Some("project_panel"); + + type FileContent = ProjectPanelSettingsContent; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result { + Self::load_via_json_merge(default_value, user_values) + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 24ec810ac5..df2da095ee 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,7 +29,7 @@ use client2::{ Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; -use dock::{Dock, DockPosition, PanelButtons}; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _}; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, @@ -937,108 +937,15 @@ impl Workspace { &self.right_dock } - // pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) - // where - // T::Event: std::fmt::Debug, - // { - // self.add_panel_with_extra_event_handler(panel, cx, |_, _, _, _| {}) - // } + pub fn add_panel(&mut self, panel: View, cx: &mut ViewContext) { + let dock = match panel.position(cx) { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; - // pub fn add_panel_with_extra_event_handler( - // &mut self, - // panel: View, - // cx: &mut ViewContext, - // handler: F, - // ) where - // T::Event: std::fmt::Debug, - // F: Fn(&mut Self, &View, &T::Event, &mut ViewContext) + 'static, - // { - // let dock = match panel.position(cx) { - // DockPosition::Left => &self.left_dock, - // DockPosition::Bottom => &self.bottom_dock, - // DockPosition::Right => &self.right_dock, - // }; - - // self.subscriptions.push(cx.subscribe(&panel, { - // let mut dock = dock.clone(); - // let mut prev_position = panel.position(cx); - // move |this, panel, event, cx| { - // if T::should_change_position_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // let new_position = panel.read(cx).position(cx); - // let mut was_visible = false; - // dock.update(cx, |dock, cx| { - // prev_position = new_position; - - // was_visible = dock.is_open() - // && dock - // .visible_panel() - // .map_or(false, |active_panel| active_panel.id() == panel.id()); - // dock.remove_panel(&panel, cx); - // }); - - // if panel.is_zoomed(cx) { - // this.zoomed_position = Some(new_position); - // } - - // dock = match panel.read(cx).position(cx) { - // DockPosition::Left => &this.left_dock, - // DockPosition::Bottom => &this.bottom_dock, - // DockPosition::Right => &this.right_dock, - // } - // .clone(); - // dock.update(cx, |dock, cx| { - // dock.add_panel(panel.clone(), cx); - // if was_visible { - // dock.set_open(true, cx); - // dock.activate_panel(dock.panels_len() - 1, cx); - // } - // }); - // } else if T::should_zoom_in_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); - // if !panel.has_focus(cx) { - // cx.focus(&panel); - // } - // this.zoomed = Some(panel.downgrade().into_any()); - // this.zoomed_position = Some(panel.read(cx).position(cx)); - // } else if T::should_zoom_out_on_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, false, cx)); - // if this.zoomed_position == Some(prev_position) { - // this.zoomed = None; - // this.zoomed_position = None; - // } - // cx.notify(); - // } else if T::is_focus_event(event) { - // THIS HAS BEEN MOVED TO NORMAL EVENT EMISSION - // See: Dock::add_panel - // - // let position = panel.read(cx).position(cx); - // this.dismiss_zoomed_items_to_reveal(Some(position), cx); - // if panel.is_zoomed(cx) { - // this.zoomed = Some(panel.downgrade().into_any()); - // this.zoomed_position = Some(position); - // } else { - // this.zoomed = None; - // this.zoomed_position = None; - // } - // this.update_active_view_for_followers(cx); - // cx.notify(); - // } else { - // handler(this, &panel, event, cx) - // } - // } - // })); - - // dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); - // } + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); + } pub fn status_bar(&self) -> &View { &self.status_bar @@ -1727,42 +1634,42 @@ impl Workspace { // } // } - // pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { - // let dock = match dock_side { - // DockPosition::Left => &self.left_dock, - // DockPosition::Bottom => &self.bottom_dock, - // DockPosition::Right => &self.right_dock, - // }; - // let mut focus_center = false; - // let mut reveal_dock = false; - // dock.update(cx, |dock, cx| { - // let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); - // let was_visible = dock.is_open() && !other_is_zoomed; - // dock.set_open(!was_visible, cx); + pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { + let dock = match dock_side { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; + let mut focus_center = false; + let mut reveal_dock = false; + dock.update(cx, |dock, cx| { + let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side); + let was_visible = dock.is_open() && !other_is_zoomed; + dock.set_open(!was_visible, cx); - // if let Some(active_panel) = dock.active_panel() { - // if was_visible { - // if active_panel.has_focus(cx) { - // focus_center = true; - // } - // } else { - // cx.focus(active_panel.as_any()); - // reveal_dock = true; - // } - // } - // }); + if let Some(active_panel) = dock.active_panel() { + if was_visible { + if active_panel.has_focus(cx) { + focus_center = true; + } + } else { + // cx.focus(active_panel.as_any()); + reveal_dock = true; + } + } + }); - // if reveal_dock { - // self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); - // } + if reveal_dock { + self.dismiss_zoomed_items_to_reveal(Some(dock_side), cx); + } - // if focus_center { - // cx.focus_self(); - // } + if focus_center { + cx.focus(&self.focus_handle); + } - // cx.notify(); - // self.serialize_workspace(cx); - // } + cx.notify(); + self.serialize_workspace(cx); + } pub fn close_all_docks(&mut self, cx: &mut ViewContext) { let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 661ab0c293..e03c44547b 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -55,7 +55,7 @@ node_runtime = { path = "../node_runtime" } # outline = { path = "../outline" } # plugin_runtime = { path = "../plugin_runtime",optional = true } project = { package = "project2", path = "../project2" } -# project_panel = { path = "../project_panel" } +project_panel = { package = "project_panel2", path = "../project_panel2" } # project_symbols = { path = "../project_symbols" } # quick_action_bar = { path = "../quick_action_bar" } # recent_projects = { path = "../recent_projects" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index cd0f8e5fbf..6a76ffac48 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -191,7 +191,7 @@ fn main() { // file_finder::init(cx); // outline::init(cx); // project_symbols::init(cx); - // project_panel::init(Assets, cx); + project_panel::init(Assets, cx); // channel::init(&client, user_store.clone(), cx); // diagnostics::init(cx); // search::init(cx); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 7368d3a5ef..bc9097dd3c 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -15,9 +15,10 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::Result; +use project_panel::ProjectPanel; use std::sync::Arc; use uuid::Uuid; -use workspace::{AppState, Workspace}; +use workspace::{dock::PanelHandle as _, AppState, Workspace}; pub fn build_window_options( bounds: Option, @@ -138,49 +139,38 @@ pub fn initialize_workspace( // } // false // }); - // })?; + })?; - // let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); - // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); - // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - // let channels_panel = - // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); - // let chat_panel = - // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); - // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( - // workspace_handle.clone(), - // cx.clone(), - // ); - // let ( - // project_panel, + let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); + // let channels_panel = + // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + // let chat_panel = + // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); + // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( + // workspace_handle.clone(), + // cx.clone(), + // ); + let ( + project_panel, // terminal_panel, // assistant_panel, // channels_panel, // chat_panel, // notification_panel, - // ) = futures::try_join!( - // project_panel, + ) = futures::try_join!( + project_panel, // terminal_panel, // assistant_panel, // channels_panel, // chat_panel, // notification_panel, - // )?; - // workspace_handle.update(&mut cx, |workspace, cx| { - // let project_panel_position = project_panel.position(cx); - // workspace.add_panel_with_extra_event_handler( - // project_panel, - // cx, - // |workspace, _, event, cx| match event { - // project_panel::Event::NewSearchInDirectory { dir_entry } => { - // search::ProjectSearchView::new_search_in_directory(workspace, dir_entry, cx) - // } - // project_panel::Event::ActivatePanel => { - // workspace.focus_panel::(cx); - // } - // _ => {} - // }, - // ); + )?; + + workspace_handle.update(&mut cx, |workspace, cx| { + let project_panel_position = project_panel.position(cx); + workspace.add_panel(project_panel, cx); // workspace.add_panel(terminal_panel, cx); // workspace.add_panel(assistant_panel, cx); // workspace.add_panel(channels_panel, cx); @@ -198,9 +188,9 @@ pub fn initialize_workspace( // .map_or(false, |entry| entry.is_dir()) // }) // { - // workspace.toggle_dock(project_panel_position, cx); + workspace.toggle_dock(project_panel_position, cx); // } - // cx.focus_self(); + // cx.focus_self(); })?; Ok(()) }) From 13255ef133c5383f81bc2db2dc2ea0efd92c9dd2 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Sun, 12 Nov 2023 23:22:02 -0800 Subject: [PATCH 02/21] Poke at getting the project panel showing up --- crates/project_panel2/src/project_panel.rs | 66 ++++--- crates/settings2/src/keymap_file.rs | 6 +- crates/workspace2/src/dock.rs | 14 +- crates/workspace2/src/modal_layer.rs | 24 --- crates/workspace2/src/workspace2.rs | 204 +++++++++++---------- 5 files changed, 157 insertions(+), 157 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 0844f5190c..e3e04f5254 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,10 +9,10 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusHandle, Model, - ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, - StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, ViewContext, - VisualContext as _, WeakView, WindowContext, + AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, + FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, + StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -131,6 +131,7 @@ pub fn init_settings(cx: &mut AppContext) { 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); @@ -1437,7 +1438,7 @@ impl ProjectPanel { } impl Render for ProjectPanel { - type Element = Div>; + type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { enum ProjectPanel {} @@ -1447,31 +1448,36 @@ impl Render for ProjectPanel { let has_worktree = self.visible_entries.len() != 0; if has_worktree { - div().id("project-panel").child( - uniform_list( - "entries", - self.visible_entries - .iter() - .map(|(_, worktree_entries)| worktree_entries.len()) - .sum(), - |this: &mut Self, range, cx| { - let mut items = SmallVec::new(); - this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - // &mut dragged_entry_destination, - cx, - )); - }); - items - }, + div() + .id("project-panel") + .track_focus(&self.focus_handle) + .child( + uniform_list( + "entries", + self.visible_entries + .iter() + .map(|(_, worktree_entries)| worktree_entries.len()) + .sum(), + |this: &mut Self, range, cx| { + let mut items = SmallVec::new(); + this.for_each_visible_entry(range, cx, |id, details, cx| { + items.push(Self::render_entry( + id, + details, + &this.filename_editor, + // &mut dragged_entry_destination, + cx, + )); + }); + items + }, + ) + .track_scroll(self.list.clone()), ) - .track_scroll(self.list.clone()), - ) } else { - v_stack().id("empty-project_panel") + v_stack() + .id("empty-project_panel") + .track_focus(&self.focus_handle) } } } @@ -1537,6 +1543,10 @@ impl workspace::dock::Panel for ProjectPanel { "Project Panel" } + fn focus_handle(&self, _cx: &WindowContext) -> FocusHandle { + self.focus_handle.clone() + } + // fn is_focus_event(event: &Self::Event) -> bool { // matches!(event, Event::Focus) // } diff --git a/crates/settings2/src/keymap_file.rs b/crates/settings2/src/keymap_file.rs index 9f279864ee..2b57af0fdb 100644 --- a/crates/settings2/src/keymap_file.rs +++ b/crates/settings2/src/keymap_file.rs @@ -9,7 +9,7 @@ use schemars::{ }; use serde::Deserialize; use serde_json::Value; -use util::{asset_str, ResultExt}; +use util::asset_str; #[derive(Debug, Deserialize, Default, Clone, JsonSchema)] #[serde(transparent)] @@ -86,7 +86,9 @@ impl KeymapFile { "invalid binding value for keystroke {keystroke}, context {context:?}" ) }) - .log_err() + // todo!() + .ok() + // .log_err() .map(|action| KeyBinding::load(&keystroke, action, context.as_deref())) }) .collect::>>()?; diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index f21eb84ae2..c8ff58f42d 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ - div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, ParentElement, Render, - Subscription, View, ViewContext, WeakView, WindowContext, + div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, FocusHandle, + ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -34,6 +34,7 @@ pub trait Panel: Render + EventEmitter { fn set_zoomed(&mut self, _zoomed: bool, _cx: &mut ViewContext) {} fn set_active(&mut self, _active: bool, _cx: &mut ViewContext) {} fn has_focus(&self, cx: &WindowContext) -> bool; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; } pub trait PanelHandle: Send + Sync { @@ -51,6 +52,7 @@ pub trait PanelHandle: Send + Sync { fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); fn icon_label(&self, cx: &WindowContext) -> Option; fn has_focus(&self, cx: &WindowContext) -> bool; + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; fn to_any(&self) -> AnyView; } @@ -117,6 +119,10 @@ where fn to_any(&self) -> AnyView { self.clone().into() } + + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + self.read(cx).focus_handle(cx).clone() + } } impl From<&dyn PanelHandle> for AnyView { @@ -728,5 +734,9 @@ pub mod test { fn has_focus(&self, _cx: &WindowContext) -> bool { self.has_focus } + + fn focus_handle(&self, cx: &WindowContext) -> FocusHandle { + unimplemented!() + } } } diff --git a/crates/workspace2/src/modal_layer.rs b/crates/workspace2/src/modal_layer.rs index a5760380f5..f197718b59 100644 --- a/crates/workspace2/src/modal_layer.rs +++ b/crates/workspace2/src/modal_layer.rs @@ -89,27 +89,3 @@ impl ModalLayer { }) } } - -// impl Render for ModalLayer { -// type Element = Div; - -// fn render(&mut self, cx: &mut ViewContext) -> Self::Element { -// let mut div = div(); -// for (type_id, build_view) in cx.global::().registered_modals { -// div = div.useful_on_action( -// type_id, -// Box::new(|this, _: dyn Any, phase, cx: &mut ViewContext| { -// if phase == DispatchPhase::Capture { -// return; -// } -// self.workspace.update(cx, |workspace, cx| { -// self.open_modal = Some(build_view(workspace, cx)); -// }); -// cx.notify(); -// }), -// ) -// } - -// div -// } -// } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index df2da095ee..c9fd322ed3 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -39,8 +39,8 @@ use gpui::{ actions, div, point, rems, size, AnyModel, AnyView, AnyWeakView, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Component, Div, Entity, EntityId, EventEmitter, FocusHandle, GlobalPixels, Model, ModelContext, ParentElement, Point, Render, Size, StatefulInteractive, - Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, WindowBounds, - WindowContext, WindowHandle, WindowOptions, + StatelessInteractive, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + WindowBounds, WindowContext, WindowHandle, WindowOptions, }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; @@ -247,102 +247,6 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { // } // } // }); - // cx.add_async_action(Workspace::open); - - // cx.add_async_action(Workspace::follow_next_collaborator); - // cx.add_async_action(Workspace::close); - // cx.add_async_action(Workspace::close_inactive_items_and_panes); - // cx.add_async_action(Workspace::close_all_items_and_panes); - // cx.add_global_action(Workspace::close_global); - // cx.add_global_action(restart); - // cx.add_async_action(Workspace::save_all); - // cx.add_action(Workspace::add_folder_to_project); - // cx.add_action( - // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { - // let pane = workspace.active_pane().clone(); - // workspace.unfollow(&pane, cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { - // workspace - // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { - // workspace - // .save_active_item(SaveIntent::SaveAs, cx) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { - // workspace.activate_previous_pane(cx) - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { - // workspace.activate_next_pane(cx) - // }); - - // cx.add_action( - // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { - // workspace.activate_pane_in_direction(action.0, cx) - // }, - // ); - - // cx.add_action( - // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { - // workspace.swap_pane_in_direction(action.0, cx) - // }, - // ); - - // cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { - // workspace.toggle_dock(DockPosition::Left, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { - // workspace.toggle_dock(DockPosition::Right, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { - // workspace.toggle_dock(DockPosition::Bottom, cx); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { - // workspace.close_all_docks(cx); - // }); - // cx.add_action(Workspace::activate_pane_at_index); - // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { - // workspace.reopen_closed_item(cx).detach(); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { - // workspace - // .go_back(workspace.active_pane().downgrade(), cx) - // .detach(); - // }); - // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { - // workspace - // .go_forward(workspace.active_pane().downgrade(), cx) - // .detach(); - // }); - - // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { - // cx.spawn(|workspace, mut cx| async move { - // let err = install_cli::install_cli(&cx) - // .await - // .context("Failed to create CLI symlink"); - - // workspace.update(&mut cx, |workspace, cx| { - // if matches!(err, Err(_)) { - // err.notify_err(workspace, cx); - // } else { - // workspace.show_notification(1, cx, |cx| { - // cx.build_view(|_| { - // MessageNotification::new("Successfully installed the `zed` binary") - // }) - // }); - // } - // }) - // }) - // .detach(); - // }); } type ProjectItemBuilders = @@ -1653,7 +1557,8 @@ impl Workspace { focus_center = true; } } else { - // cx.focus(active_panel.as_any()); + let focus_handle = &active_panel.focus_handle(cx); + cx.focus(focus_handle); reveal_dock = true; } } @@ -3350,6 +3255,103 @@ impl Workspace { }) } + fn actions(div: Div) -> Div { + div + // cx.add_async_action(Workspace::open); + // cx.add_async_action(Workspace::follow_next_collaborator); + // cx.add_async_action(Workspace::close); + // cx.add_async_action(Workspace::close_inactive_items_and_panes); + // cx.add_async_action(Workspace::close_all_items_and_panes); + // cx.add_global_action(Workspace::close_global); + // cx.add_global_action(restart); + // cx.add_async_action(Workspace::save_all); + // cx.add_action(Workspace::add_folder_to_project); + // cx.add_action( + // |workspace: &mut Workspace, _: &Unfollow, cx: &mut ViewContext| { + // let pane = workspace.active_pane().clone(); + // workspace.unfollow(&pane, cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &Save, cx: &mut ViewContext| { + // workspace + // .save_active_item(action.save_intent.unwrap_or(SaveIntent::Save), cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, _: &SaveAs, cx: &mut ViewContext| { + // workspace + // .save_active_item(SaveIntent::SaveAs, cx) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { + // workspace.activate_previous_pane(cx) + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { + // workspace.activate_next_pane(cx) + // }); + // cx.add_action( + // |workspace: &mut Workspace, action: &ActivatePaneInDirection, cx| { + // workspace.activate_pane_in_direction(action.0, cx) + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, action: &SwapPaneInDirection, cx| { + // workspace.swap_pane_in_direction(action.0, cx) + // }, + // ); + .on_action(|this, e: &ToggleLeftDock, cx| { + println!("TOGGLING DOCK"); + this.toggle_dock(DockPosition::Left, cx); + }) + // cx.add_action(|workspace: &mut Workspace, _: &ToggleRightDock, cx| { + // workspace.toggle_dock(DockPosition::Right, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &ToggleBottomDock, cx| { + // workspace.toggle_dock(DockPosition::Bottom, cx); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &CloseAllDocks, cx| { + // workspace.close_all_docks(cx); + // }); + // cx.add_action(Workspace::activate_pane_at_index); + // cx.add_action(|workspace: &mut Workspace, _: &ReopenClosedItem, cx| { + // workspace.reopen_closed_item(cx).detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoBack, cx| { + // workspace + // .go_back(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + // cx.add_action(|workspace: &mut Workspace, _: &GoForward, cx| { + // workspace + // .go_forward(workspace.active_pane().downgrade(), cx) + // .detach(); + // }); + + // cx.add_action(|_: &mut Workspace, _: &install_cli::Install, cx| { + // cx.spawn(|workspace, mut cx| async move { + // let err = install_cli::install_cli(&cx) + // .await + // .context("Failed to create CLI symlink"); + + // workspace.update(&mut cx, |workspace, cx| { + // if matches!(err, Err(_)) { + // err.notify_err(workspace, cx); + // } else { + // workspace.show_notification(1, cx, |cx| { + // cx.build_view(|_| { + // MessageNotification::new("Successfully installed the `zed` binary") + // }) + // }); + // } + // }) + // }) + // .detach(); + // }); + } + // todo!() // #[cfg(any(test, feature = "test-support"))] // pub fn test_new(project: ModelHandle, cx: &mut ViewContext) -> Self { @@ -3628,7 +3630,7 @@ impl Render for Workspace { .text_color(cx.theme().colors().text) .bg(cx.theme().colors().background) .child(self.render_titlebar(cx)) - .child( + .child(Workspace::actions( // todo! should this be a component a view? self.modal_layer .wrapper_element(cx) @@ -3717,7 +3719,7 @@ impl Render for Workspace { // ) // .filter(|_| self.is_assistant_panel_open()), // ), - ) + )) .child(self.status_bar.clone()) // .when(self.debug.show_toast, |this| { // this.child(Toast::new(ToastOrigin::Bottom).child(Label::new("A toast"))) From 13dd9128170b70e95d0d163f06b6bcd8a9013425 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Nov 2023 10:47:15 -0800 Subject: [PATCH 03/21] Get left, right, and bottom docks rendering in the right places in the workspace Co-authored-by: Julia Co-authored-by: Marshall --- crates/gpui2/src/color.rs | 18 +++++++ crates/project_panel2/src/project_panel.rs | 6 ++- crates/workspace2/src/dock.rs | 8 ++- crates/workspace2/src/workspace2.rs | 61 ++++++++-------------- 4 files changed, 51 insertions(+), 42 deletions(-) diff --git a/crates/gpui2/src/color.rs b/crates/gpui2/src/color.rs index 6fcb12e178..d8989878de 100644 --- a/crates/gpui2/src/color.rs +++ b/crates/gpui2/src/color.rs @@ -238,6 +238,24 @@ pub fn blue() -> Hsla { } } +pub fn green() -> Hsla { + Hsla { + h: 0.33, + s: 1., + l: 0.5, + a: 1., + } +} + +pub fn yellow() -> Hsla { + Hsla { + h: 0.16, + s: 1., + l: 0.5, + a: 1., + } +} + impl Hsla { /// Returns true if the HSLA color is fully transparent, false otherwise. pub fn is_transparent(&self) -> bool { diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index e3e04f5254..fec44e3d68 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -31,7 +31,7 @@ use std::{ sync::Arc, }; use theme::ActiveTheme as _; -use ui::{h_stack, v_stack}; +use ui::{h_stack, v_stack, Label}; use unicase::UniCase; use util::TryFutureExt; use workspace::{ @@ -1382,7 +1382,7 @@ impl ProjectPanel { if let (Some(editor), true) = (editor, show_editor) { div().child(editor.clone()) } else { - div().child(details.filename.clone()) + div().child(Label::new(details.filename.clone())) } .ml_1(), ) @@ -1449,6 +1449,7 @@ impl Render for ProjectPanel { if has_worktree { div() + .size_full() .id("project-panel") .track_focus(&self.focus_handle) .child( @@ -1472,6 +1473,7 @@ impl Render for ProjectPanel { items }, ) + .size_full() .track_scroll(self.list.clone()), ) } else { diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index c8ff58f42d..9a614bc92e 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -1,7 +1,7 @@ use crate::{status_bar::StatusItemView, Axis, Workspace}; use gpui::{ div, Action, AnyView, AppContext, Div, Entity, EntityId, EventEmitter, FocusHandle, - ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext, + ParentElement, Render, Styled, Subscription, View, ViewContext, WeakView, WindowContext, }; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -428,7 +428,11 @@ impl Render for Dock { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - todo!() + if let Some(entry) = self.visible_entry() { + div().size_full().child(entry.panel.to_any()) + } else { + div() + } } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index c9fd322ed3..8c1c5d9a02 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -3642,45 +3642,30 @@ impl Render for Workspace { .border_t() .border_b() .border_color(cx.theme().colors().border) - // .children( - // Some( - // Panel::new("project-panel-outer", cx) - // .side(PanelSide::Left) - // .child(ProjectPanel::new("project-panel-inner")), - // ) - // .filter(|_| self.is_project_panel_open()), - // ) - // .children( - // Some( - // Panel::new("collab-panel-outer", cx) - // .child(CollabPanel::new("collab-panel-inner")) - // .side(PanelSide::Left), - // ) - // .filter(|_| self.is_collab_panel_open()), - // ) - // .child(NotificationToast::new( - // "maxbrunsfeld has requested to add you as a contact.".into(), - // )) .child( - div().flex().flex_col().flex_1().h_full().child( - div().flex().flex_1().child(self.center.render( - &self.project, - &self.follower_states, - self.active_call(), - &self.active_pane, - self.zoomed.as_ref(), - &self.app_state, - cx, - )), - ), // .children( - // Some( - // Panel::new("terminal-panel", cx) - // .child(Terminal::new()) - // .allowed_sides(PanelAllowedSides::BottomOnly) - // .side(PanelSide::Bottom), - // ) - // .filter(|_| self.is_terminal_open()), - // ), + div() + .flex() + .flex_row() + .flex_1() + .h_full() + .child(div().flex().flex_1().child(self.left_dock.clone())) + .child( + div() + .flex() + .flex_col() + .flex_1() + .child(self.center.render( + &self.project, + &self.follower_states, + self.active_call(), + &self.active_pane, + self.zoomed.as_ref(), + &self.app_state, + cx, + )) + .child(div().flex().flex_1().child(self.bottom_dock.clone())), + ) + .child(div().flex().flex_1().child(self.right_dock.clone())), ), // .children( // Some( // Panel::new("chat-panel-outer", cx) From 91b634fc59ef7f0ed52ac7a23d345505a0de64b7 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Nov 2023 11:08:51 -0800 Subject: [PATCH 04/21] Get some project panel actions working Add styling for hovered and selected items Co-authored-by: Marshall Co-authored-by: Julia --- crates/project_panel2/src/project_panel.rs | 88 ++++++++++++---------- 1 file changed, 50 insertions(+), 38 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index fec44e3d68..22d9d31336 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,11 +8,11 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Div, Element, Entity, EventEmitter, FocusEnabled, + actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, + AsyncWindowContext, ClipboardItem, Component, Div, Element, Entity, EventEmitter, FocusEnabled, FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, - StatefulInteractive, StatefulInteractivity, Styled, Task, UniformListScrollHandle, View, - ViewContext, VisualContext as _, WeakView, WindowContext, + StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, + UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -132,25 +132,6 @@ pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); - // cx.add_action(ProjectPanel::expand_selected_entry); - // cx.add_action(ProjectPanel::collapse_selected_entry); - // cx.add_action(ProjectPanel::collapse_all_entries); - // cx.add_action(ProjectPanel::select_prev); - // cx.add_action(ProjectPanel::select_next); - // cx.add_action(ProjectPanel::new_file); - // cx.add_action(ProjectPanel::new_directory); - // cx.add_action(ProjectPanel::rename); - // cx.add_async_action(ProjectPanel::delete); - // cx.add_async_action(ProjectPanel::confirm); - // cx.add_async_action(ProjectPanel::open_file); - // cx.add_action(ProjectPanel::cancel); - // cx.add_action(ProjectPanel::cut); - // cx.add_action(ProjectPanel::copy); - // cx.add_action(ProjectPanel::copy_path); - // cx.add_action(ProjectPanel::copy_relative_path); - // cx.add_action(ProjectPanel::reveal_in_finder); - // cx.add_action(ProjectPanel::open_in_terminal); - // cx.add_action(ProjectPanel::new_search_in_directory); // cx.add_action( // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { // this.paste(action, cx); @@ -1366,15 +1347,22 @@ impl ProjectPanel { .git_status .as_ref() .map(|status| match status { - GitFileStatus::Added => theme.styles.status.created, - GitFileStatus::Modified => theme.styles.status.modified, - GitFileStatus::Conflict => theme.styles.status.conflict, + GitFileStatus::Added => theme.status().created, + GitFileStatus::Modified => theme.status().modified, + GitFileStatus::Conflict => theme.status().conflict, }) - .unwrap_or(theme.styles.status.info); + .unwrap_or(theme.status().info); h_stack() .child(if let Some(icon) = &details.icon { - div().child(svg().path(icon.to_string())) + div().child( + // todo!() Marshall: Can we use our `IconElement` component here? + svg() + .size(rems(0.9375)) + .flex_none() + .path(icon.to_string()) + .text_color(cx.theme().colors().icon), + ) } else { div() }) @@ -1390,11 +1378,10 @@ impl ProjectPanel { } fn render_entry( + &self, entry_id: ProjectEntryId, details: EntryDetails, - editor: &View, // dragged_entry_destination: &mut Option>, - // theme: &theme::ProjectPanel, cx: &mut ViewContext, ) -> Div> { let kind = details.kind; @@ -1402,9 +1389,18 @@ impl ProjectPanel { const INDENT_SIZE: Pixels = px(16.0); let padding = INDENT_SIZE + details.depth as f32 * px(settings.indent_size); let show_editor = details.is_editing && !details.is_processing; + let is_selected = self + .selection + .map_or(false, |selection| selection.entry_id == entry_id); - Self::render_entry_visual_element(&details, Some(editor), padding, cx) + Self::render_entry_visual_element(&details, Some(&self.filename_editor), padding, cx) .id(entry_id.to_proto() as usize) + .w_full() + .cursor_pointer() + .when(is_selected, |this| { + this.bg(cx.theme().colors().element_selected) + }) + .hover(|style| style.bg(cx.theme().colors().element_hover)) .on_click(move |this, event, cx| { if !show_editor { if kind.is_dir() { @@ -1441,7 +1437,6 @@ impl Render for ProjectPanel { type Element = Div, FocusEnabled>; fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - enum ProjectPanel {} let theme = cx.theme(); let last_worktree_root_id = self.last_worktree_root_id; @@ -1449,8 +1444,28 @@ impl Render for ProjectPanel { if has_worktree { div() - .size_full() .id("project-panel") + .size_full() + .context("ProjectPanel") + .on_action(Self::select_next) + .on_action(Self::select_prev) + .on_action(Self::expand_selected_entry) + .on_action(Self::collapse_selected_entry) + .on_action(Self::collapse_all_entries) + .on_action(Self::new_file) + .on_action(Self::new_directory) + .on_action(Self::rename) + // .on_action(Self::delete) + // .on_action(Self::confirm) + // .on_action(Self::open_file) + .on_action(Self::cancel) + .on_action(Self::cut) + .on_action(Self::copy) + .on_action(Self::copy_path) + .on_action(Self::copy_relative_path) + .on_action(Self::reveal_in_finder) + .on_action(Self::open_in_terminal) + .on_action(Self::new_search_in_directory) .track_focus(&self.focus_handle) .child( uniform_list( @@ -1462,11 +1477,8 @@ impl Render for ProjectPanel { |this: &mut Self, range, cx| { let mut items = SmallVec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(Self::render_entry( - id, - details, - &this.filename_editor, - // &mut dragged_entry_destination, + items.push(this.render_entry( + id, details, // &mut dragged_entry_destination, cx, )); }); From c0f34e33029ec1f411074628397552bc697cb93e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 13 Nov 2023 11:38:47 -0800 Subject: [PATCH 05/21] Add delete, open file, and rename actions to the project panel Co-authored-by: Julia Co-authored-by: Marshall --- crates/project_panel2/src/project_panel.rs | 96 +++++++++++----------- 1 file changed, 47 insertions(+), 49 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 22d9d31336..b963625978 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -8,11 +8,11 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ - actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncAppContext, - AsyncWindowContext, ClipboardItem, Component, Div, Element, Entity, EventEmitter, FocusEnabled, - FocusHandle, Model, ParentElement as _, Pixels, Point, PromptLevel, Render, - StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, - UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, + actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, + ClipboardItem, Component, Div, Entity, EventEmitter, FocusEnabled, FocusHandle, Model, + ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, + StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, + ViewContext, VisualContext as _, WeakView, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ @@ -33,7 +33,7 @@ use std::{ use theme::ActiveTheme as _; use ui::{h_stack, v_stack, Label}; use unicase::UniCase; -use util::TryFutureExt; +use util::{maybe, TryFutureExt}; use workspace::{ dock::{DockPosition, PanelEvent}, Workspace, @@ -131,12 +131,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); - - // cx.add_action( - // |this: &mut ProjectPanel, action: &Paste, cx: &mut ViewContext| { - // this.paste(action, cx); - // }, - // ); } #[derive(Debug)] @@ -560,22 +554,18 @@ impl ProjectPanel { } } - fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) -> Option>> { + fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { if let Some(task) = self.confirm_edit(cx) { - return Some(task); + task.detach_and_log_err(cx); } - - None } - fn open_file(&mut self, _: &Open, cx: &mut ViewContext) -> Option>> { + fn open_file(&mut self, _: &Open, cx: &mut ViewContext) { if let Some((_, entry)) = self.selected_entry(cx) { if entry.is_file() { self.open_entry(entry.id, true, cx); } } - - None } fn confirm_edit(&mut self, cx: &mut ViewContext) -> Option>> { @@ -781,27 +771,32 @@ impl ProjectPanel { } } - fn delete(&mut self, _: &Delete, cx: &mut ViewContext) -> Option>> { - let Selection { entry_id, .. } = self.selection?; - let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; - let file_name = path.file_name()?; + fn delete(&mut self, _: &Delete, cx: &mut ViewContext) { + maybe!({ + let Selection { entry_id, .. } = self.selection?; + let path = self.project.read(cx).path_for_entry(entry_id, cx)?.path; + let file_name = path.file_name()?; - let mut answer = cx.prompt( - PromptLevel::Info, - &format!("Delete {file_name:?}?"), - &["Delete", "Cancel"], - ); - Some(cx.spawn(|this, mut cx| async move { - if answer.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 - })) + 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) { @@ -878,8 +873,9 @@ impl ProjectPanel { } } - fn paste(&mut self, _: &Paste, cx: &mut ViewContext) -> Option<()> { - if let Some((worktree, entry)) = self.selected_entry(cx) { + fn paste(&mut self, _: &Paste, cx: &mut ViewContext) { + maybe!({ + let (worktree, entry) = self.selected_entry(cx)?; let clipboard_entry = self.clipboard_entry?; if clipboard_entry.worktree_id() != worktree.id() { return None; @@ -923,15 +919,16 @@ impl ProjectPanel { if let Some(task) = self.project.update(cx, |project, cx| { project.rename_entry(clipboard_entry.entry_id(), new_path, cx) }) { - task.detach_and_log_err(cx) + task.detach_and_log_err(cx); } } else if let Some(task) = self.project.update(cx, |project, cx| { project.copy_entry(clipboard_entry.entry_id(), new_path, cx) }) { - task.detach_and_log_err(cx) + task.detach_and_log_err(cx); } - } - None + + Some(()) + }); } fn copy_path(&mut self, _: &CopyPath, cx: &mut ViewContext) { @@ -1368,7 +1365,7 @@ impl ProjectPanel { }) .child( if let (Some(editor), true) = (editor, show_editor) { - div().child(editor.clone()) + div().w_full().child(editor.clone()) } else { div().child(Label::new(details.filename.clone())) } @@ -1455,14 +1452,15 @@ impl Render for ProjectPanel { .on_action(Self::new_file) .on_action(Self::new_directory) .on_action(Self::rename) - // .on_action(Self::delete) - // .on_action(Self::confirm) - // .on_action(Self::open_file) + .on_action(Self::delete) + .on_action(Self::confirm) + .on_action(Self::open_file) .on_action(Self::cancel) .on_action(Self::cut) .on_action(Self::copy) .on_action(Self::copy_path) .on_action(Self::copy_relative_path) + .on_action(Self::paste) .on_action(Self::reveal_in_finder) .on_action(Self::open_in_terminal) .on_action(Self::new_search_in_directory) From ec16e70336552255adf99671ca4d3c4e3d1b5c5d Mon Sep 17 00:00:00 2001 From: Mikayla Date: Mon, 13 Nov 2023 17:34:12 -0800 Subject: [PATCH 06/21] Sketch in core zed actions code --- crates/Cargo.toml | 38 ---- crates/workspace2/src/workspace2.rs | 5 +- crates/zed2/src/main.rs | 7 +- crates/zed2/src/zed2.rs | 257 ++++++++++++++++++++++++++++ crates/zed_actions2/src/lib.rs | 9 + 5 files changed, 274 insertions(+), 42 deletions(-) delete mode 100644 crates/Cargo.toml diff --git a/crates/Cargo.toml b/crates/Cargo.toml deleted file mode 100644 index fb49a4b515..0000000000 --- a/crates/Cargo.toml +++ /dev/null @@ -1,38 +0,0 @@ -[package] -name = "ai" -version = "0.1.0" -edition = "2021" -publish = false - -[lib] -path = "src/ai.rs" -doctest = false - -[features] -test-support = [] - -[dependencies] -gpui = { path = "../gpui" } -util = { path = "../util" } -language = { path = "../language" } -async-trait.workspace = true -anyhow.workspace = true -futures.workspace = true -lazy_static.workspace = true -ordered-float.workspace = true -parking_lot.workspace = true -isahc.workspace = true -regex.workspace = true -serde.workspace = true -serde_json.workspace = true -postage.workspace = true -rand.workspace = true -log.workspace = true -parse_duration = "2.1.1" -tiktoken-rs = "0.5.0" -matrixmultiply = "0.3.7" -rusqlite = { version = "0.29.0", features = ["blob", "array", "modern_sqlite"] } -bincode = "1.3.3" - -[dev-dependencies] -gpui = { path = "../gpui", features = ["test-support"] } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 2ddd210c9f..f5cbc6e787 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -72,7 +72,7 @@ pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use ui::{h_stack, Label}; use util::ResultExt; use uuid::Uuid; -use workspace_settings::{AutosaveSetting, WorkspaceSettings}; +pub use workspace_settings::{AutosaveSetting, WorkspaceSettings}; lazy_static! { static ref ZED_WINDOW_SIZE: Option> = env::var("ZED_WINDOW_SIZE") @@ -3506,13 +3506,14 @@ impl Workspace { pub fn register_action( &mut self, callback: impl Fn(&mut Self, &A, &mut ViewContext) + 'static, - ) { + ) -> &mut Self { let callback = Arc::new(callback); self.workspace_actions.push(Box::new(move |div| { let callback = callback.clone(); div.on_action(move |workspace, event, cx| (callback.clone())(workspace, event, cx)) })); + self } fn add_workspace_actions_listeners( diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 2deaff2149..9a4ad81806 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -50,14 +50,16 @@ use util::{ use uuid::Uuid; use workspace::{AppState, WorkspaceStore}; use zed2::{ - build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, - languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, + build_window_options, ensure_only_instance, handle_cli_connection, init_zed_actions, + initialize_workspace, languages, Assets, IsOnlyInstance, OpenListener, OpenRequest, }; mod open_listener; fn main() { menu::init(); + zed_actions::init(); + let http = http::client(); init_paths(); init_logger(); @@ -209,6 +211,7 @@ fn main() { // zed::init(&app_state, cx); // cx.set_menus(menus::menus()); + init_zed_actions(cx); if stdout_is_a_pty() { cx.activate(true); diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index de985496c8..54723ee8d8 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -15,6 +15,7 @@ pub use only_instance::*; pub use open_listener::*; use anyhow::Result; +use settings::Settings; use std::sync::Arc; use uuid::Uuid; use workspace::{AppState, Workspace}; @@ -47,6 +48,214 @@ pub fn build_window_options( } } +pub fn init_zed_actions(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, cx| { + workspace + // cx.add_action(about); + // cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| { + // cx.platform().hide(); + // }); + // cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| { + // cx.platform().hide_other_apps(); + // }); + // cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| { + // cx.platform().unhide_other_apps(); + // }); + // cx.add_action( + // |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext| { + // cx.minimize_window(); + // }, + // ); + // cx.add_action( + // |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext| { + // cx.zoom_window(); + // }, + // ); + // cx.add_action( + // |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext| { + // cx.toggle_full_screen(); + // }, + // ); + .register_action(|workspace, _: &zed_actions::Quit, cx| quit(cx)); + // cx.add_global_action(move |action: &OpenZedURL, cx| { + // cx.global::>() + // .open_urls(vec![action.url.clone()]) + // }); + // cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); + // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size += 1.0) + // }); + // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size -= 1.0) + // }); + // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + // cx.add_global_action(move |_: &install_cli::Install, cx| { + // cx.spawn(|cx| async move { + // install_cli::install_cli(&cx) + // .await + // .context("error creating CLI symlink") + // }) + // .detach_and_log_err(cx); + // }); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { + // open_log_file(workspace, cx); + // }, + // ); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { + // open_bundled_file( + // workspace, + // asset_str::("licenses.md"), + // "Open Source License Attribution", + // "Markdown", + // cx, + // ); + // }, + // ); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext| { + // open_telemetry_log_file(workspace, cx); + // }, + // ); + // cx.add_action( + // move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { + // create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx); + // }, + // ); + // cx.add_action( + // move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + // create_and_open_local_file(&paths::SETTINGS, cx, || { + // settings::initial_user_settings_content().as_ref().into() + // }) + // .detach_and_log_err(cx); + // }, + // ); + // cx.add_action(open_local_settings_file); + // cx.add_action( + // move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { + // open_bundled_file( + // workspace, + // settings::default_keymap(), + // "Default Key Bindings", + // "JSON", + // cx, + // ); + // }, + // ); + // cx.add_action( + // move |workspace: &mut Workspace, + // _: &OpenDefaultSettings, + // cx: &mut ViewContext| { + // open_bundled_file( + // workspace, + // settings::default_settings(), + // "Default Settings", + // "JSON", + // cx, + // ); + // }, + // ); + // cx.add_action({ + // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + // let app_state = workspace.app_state().clone(); + // let markdown = app_state.languages.language_for_name("JSON"); + // let window = cx.window(); + // cx.spawn(|workspace, mut cx| async move { + // let markdown = markdown.await.log_err(); + // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { + // anyhow!("could not debug elements for window {}", window.id()) + // })?) + // .unwrap(); + // workspace + // .update(&mut cx, |workspace, cx| { + // workspace.with_local_workspace(cx, move |workspace, cx| { + // let project = workspace.project().clone(); + + // let buffer = project + // .update(cx, |project, cx| { + // project.create_buffer(&content, markdown, cx) + // }) + // .expect("creating buffers on a local workspace always succeeds"); + // let buffer = cx.add_model(|cx| { + // MultiBuffer::singleton(buffer, cx) + // .with_title("Debug Elements".into()) + // }); + // workspace.add_item( + // Box::new(cx.add_view(|cx| { + // Editor::for_multibuffer(buffer, Some(project.clone()), cx) + // })), + // cx, + // ); + // }) + // })? + // .await + // }) + // .detach_and_log_err(cx); + // } + // }); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &project_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::collab_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::chat_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::notification_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &terminal_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_global_action({ + // let app_state = Arc::downgrade(&app_state); + // move |_: &NewWindow, cx: &mut AppContext| { + // if let Some(app_state) = app_state.upgrade() { + // open_new(&app_state, cx, |workspace, cx| { + // Editor::new_file(workspace, &Default::default(), cx) + // }) + // .detach(); + // } + // } + // }); + // cx.add_global_action({ + // let app_state = Arc::downgrade(&app_state); + // move |_: &NewFile, cx: &mut AppContext| { + // if let Some(app_state) = app_state.upgrade() { + // open_new(&app_state, cx, |workspace, cx| { + // Editor::new_file(workspace, &Default::default(), cx) + // }) + // .detach(); + // } + // } + // }); + // load_default_keymap(cx); + }) + .detach(); +} + pub fn initialize_workspace( workspace_handle: WeakView, was_deserialized: bool, @@ -205,3 +414,51 @@ pub fn initialize_workspace( Ok(()) }) } + +fn quit(cx: &mut gpui::AppContext) { + let should_confirm = workspace::WorkspaceSettings::get_global(cx).confirm_quit; + cx.spawn(|mut cx| async move { + // let mut workspace_windows = cx + // .windows() + // .into_iter() + // .filter_map(|window| window.downcast::()) + // .collect::>(); + + // // If multiple windows have unsaved changes, and need a save prompt, + // // prompt in the active window before switching to a different window. + // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + + // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + // let answer = window.prompt( + // PromptLevel::Info, + // "Are you sure you want to quit?", + // &["Quit", "Cancel"], + // &mut cx, + // ); + + // if let Some(mut answer) = answer { + // let answer = answer.next().await; + // if answer != Some(0) { + // return Ok(()); + // } + // } + // } + + // // If the user cancels any save prompt, then keep the app open. + // for window in workspace_windows { + // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { + // workspace.prepare_to_close(true, cx) + // }) { + // if !should_close.await? { + // return Ok(()); + // } + // } + // } + cx.update(|cx| { + cx.quit(); + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); +} diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 090352b2cc..097766492f 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -1,5 +1,14 @@ use gpui::{action, actions}; +// If the zed binary doesn't use anything in this crate, it will be optimized away +// and the actions won't initialize. So we just provide an empty initialization function +// to be called from main. +// +// These may provide relevant context: +// https://github.com/rust-lang/rust/issues/47384 +// https://github.com/mmastrac/rust-ctor/issues/280 +pub fn init() {} + actions!( About, DebugElements, From ee4957dd477c638f24c197508271aed7ac4f6861 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 00:06:33 -0800 Subject: [PATCH 07/21] Implement most core actions --- Cargo.lock | 3 +- crates/gpui/src/app.rs | 4 + crates/gpui2/src/app.rs | 12 + crates/gpui2/src/window.rs | 8 + crates/install_cli2/Cargo.toml | 1 + crates/install_cli2/src/install_cli2.rs | 5 +- crates/workspace2/src/workspace2.rs | 96 ++-- crates/zed2/Cargo.toml | 2 +- crates/zed2/src/main.rs | 8 +- crates/zed2/src/open_listener.rs | 4 +- crates/zed2/src/zed2.rs | 715 ++++++++++++++++-------- crates/zed_actions2/src/lib.rs | 27 +- 12 files changed, 570 insertions(+), 315 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 97653e124a..a35dfd20cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4198,6 +4198,7 @@ dependencies = [ "anyhow", "gpui2", "log", + "serde", "smol", "util", ] @@ -11402,7 +11403,7 @@ dependencies = [ "ignore", "image", "indexmap 1.9.3", - "install_cli", + "install_cli2", "isahc", "journal2", "language2", diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 4b6b9bea73..b732be7455 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -2112,6 +2112,10 @@ impl AppContext { AsyncAppContext(self.weak_self.as_ref().unwrap().upgrade().unwrap()) } + pub fn open_url(&self, url: &str) { + self.platform.open_url(url) + } + pub fn write_to_clipboard(&self, item: ClipboardItem) { self.platform.write_to_clipboard(item); } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index b6cb3f6307..5463550587 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -431,6 +431,18 @@ impl AppContext { self.platform.activate(ignoring_other_apps); } + pub fn hide(&self) { + self.platform.hide(); + } + + pub fn hide_other_apps(&self) { + self.platform.hide_other_apps(); + } + + pub fn unhide_other_apps(&self) { + self.platform.unhide_other_apps(); + } + /// Returns the list of currently active displays. pub fn displays(&self) -> Vec> { self.platform.displays() diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index eb69b451b3..4ad807b357 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -1365,6 +1365,14 @@ impl<'a> WindowContext<'a> { self.window.platform_window.activate(); } + pub fn minimize_window(&self) { + self.window.platform_window.minimize(); + } + + pub fn toggle_full_screen(&self) { + self.window.platform_window.toggle_full_screen(); + } + pub fn prompt( &self, level: PromptLevel, diff --git a/crates/install_cli2/Cargo.toml b/crates/install_cli2/Cargo.toml index 3310e7fbc8..26fe212fe3 100644 --- a/crates/install_cli2/Cargo.toml +++ b/crates/install_cli2/Cargo.toml @@ -14,5 +14,6 @@ test-support = [] smol.workspace = true anyhow.workspace = true log.workspace = true +serde.workspace = true gpui = { package = "gpui2", path = "../gpui2" } util = { path = "../util" } diff --git a/crates/install_cli2/src/install_cli2.rs b/crates/install_cli2/src/install_cli2.rs index 7938d60210..6fd1019c3f 100644 --- a/crates/install_cli2/src/install_cli2.rs +++ b/crates/install_cli2/src/install_cli2.rs @@ -1,10 +1,9 @@ use anyhow::{anyhow, Result}; -use gpui::AsyncAppContext; +use gpui::{actions, AsyncAppContext}; use std::path::Path; use util::ResultExt; -// todo!() -// actions!(cli, [Install]); +actions!(Install); pub async fn install_cli(cx: &AsyncAppContext) -> Result<()> { let cli_path = cx.update(|cx| cx.path_for_auxiliary_executable("cli"))??; diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index f5cbc6e787..1a5f3329b8 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -45,7 +45,7 @@ use gpui::{ }; use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem}; use itertools::Itertools; -use language2::LanguageRegistry; +use language2::{LanguageRegistry, Rope}; use lazy_static::lazy_static; pub use modal_layer::*; use node_runtime::NodeRuntime; @@ -1241,29 +1241,29 @@ impl Workspace { // self.titlebar_item.clone() // } - // /// Call the given callback with a workspace whose project is local. - // /// - // /// If the given workspace has a local project, then it will be passed - // /// to the callback. Otherwise, a new empty window will be created. - // pub fn with_local_workspace( - // &mut self, - // cx: &mut ViewContext, - // callback: F, - // ) -> Task> - // where - // T: 'static, - // F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, - // { - // if self.project.read(cx).is_local() { - // Task::Ready(Some(Ok(callback(self, cx)))) - // } else { - // let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); - // cx.spawn(|_vh, mut cx| async move { - // let (workspace, _) = task.await; - // workspace.update(&mut cx, callback) - // }) - // } - // } + /// Call the given callback with a workspace whose project is local. + /// + /// If the given workspace has a local project, then it will be passed + /// to the callback. Otherwise, a new empty window will be created. + pub fn with_local_workspace( + &mut self, + cx: &mut ViewContext, + callback: F, + ) -> Task> + where + T: 'static, + F: 'static + FnOnce(&mut Workspace, &mut ViewContext) -> T, + { + if self.project.read(cx).is_local() { + Task::Ready(Some(Ok(callback(self, cx)))) + } else { + let task = Self::new_local(Vec::new(), self.app_state.clone(), None, cx); + cx.spawn(|_vh, mut cx| async move { + let (workspace, _) = task.await?; + workspace.update(&mut cx, callback) + }) + } + } pub fn worktrees<'a>(&self, cx: &'a AppContext) -> impl 'a + Iterator> { self.project.read(cx).worktrees() @@ -4507,32 +4507,32 @@ pub fn open_new( }) } -// pub fn create_and_open_local_file( -// path: &'static Path, -// cx: &mut ViewContext, -// default_content: impl 'static + Send + FnOnce() -> Rope, -// ) -> Task>> { -// cx.spawn(|workspace, mut cx| async move { -// let fs = workspace.read_with(&cx, |workspace, _| workspace.app_state().fs.clone())?; -// if !fs.is_file(path).await { -// fs.create_file(path, Default::default()).await?; -// fs.save(path, &default_content(), Default::default()) -// .await?; -// } +pub fn create_and_open_local_file( + path: &'static Path, + cx: &mut ViewContext, + default_content: impl 'static + Send + FnOnce() -> Rope, +) -> Task>> { + cx.spawn(|workspace, mut cx| async move { + let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?; + if !fs.is_file(path).await { + fs.create_file(path, Default::default()).await?; + fs.save(path, &default_content(), Default::default()) + .await?; + } -// let mut items = workspace -// .update(&mut cx, |workspace, cx| { -// workspace.with_local_workspace(cx, |workspace, cx| { -// workspace.open_paths(vec![path.to_path_buf()], false, cx) -// }) -// })? -// .await? -// .await; + let mut items = workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(cx, |workspace, cx| { + workspace.open_paths(vec![path.to_path_buf()], false, cx) + }) + })? + .await? + .await; -// let item = items.pop().flatten(); -// item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? -// }) -// } + let item = items.pop().flatten(); + item.ok_or_else(|| anyhow!("path {path:?} is not a file"))? + }) +} // pub fn join_remote_project( // project_id: u64, diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 570912abc5..610bd90f69 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -43,7 +43,7 @@ fsevent = { path = "../fsevent" } fuzzy = { path = "../fuzzy" } go_to_line = { package = "go_to_line2", path = "../go_to_line2" } gpui = { package = "gpui2", path = "../gpui2" } -install_cli = { path = "../install_cli" } +install_cli = { package = "install_cli2", path = "../install_cli2" } journal = { package = "journal2", path = "../journal2" } language = { package = "language2", path = "../language2" } # language_selector = { path = "../language_selector" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 9a4ad81806..730dd255f9 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -98,7 +98,7 @@ fn main() { let (listener, mut open_rx) = OpenListener::new(); let listener = Arc::new(listener); let open_listener = listener.clone(); - app.on_open_urls(move |urls, _| open_listener.open_urls(urls)); + app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); app.on_reopen(move |_cx| { // todo!("workspace") // if cx.has_global::>() { @@ -211,13 +211,13 @@ fn main() { // zed::init(&app_state, cx); // cx.set_menus(menus::menus()); - init_zed_actions(cx); + init_zed_actions(app_state.clone(), cx); if stdout_is_a_pty() { cx.activate(true); let urls = collect_url_args(); if !urls.is_empty() { - listener.open_urls(urls) + listener.open_urls(&urls) } } else { upload_previous_panics(http.clone(), cx); @@ -227,7 +227,7 @@ fn main() { if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() && !listener.triggered.load(Ordering::Acquire) { - listener.open_urls(collect_url_args()) + listener.open_urls(&collect_url_args()) } } diff --git a/crates/zed2/src/open_listener.rs b/crates/zed2/src/open_listener.rs index f4219f199d..4c961a2b31 100644 --- a/crates/zed2/src/open_listener.rs +++ b/crates/zed2/src/open_listener.rs @@ -54,7 +54,7 @@ impl OpenListener { ) } - pub fn open_urls(&self, urls: Vec) { + pub fn open_urls(&self, urls: &[String]) { self.triggered.store(true, Ordering::Release); let request = if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) @@ -101,7 +101,7 @@ impl OpenListener { None } - fn handle_file_urls(&self, urls: Vec) -> Option { + fn handle_file_urls(&self, urls: &[String]) -> Option { let paths: Vec<_> = urls .iter() .flat_map(|url| url.strip_prefix("file://")) diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 54723ee8d8..37c317fb61 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -1,5 +1,5 @@ -#![allow(unused_variables, dead_code, unused_mut)] -// todo!() this is to make transition easier. +#![allow(unused_variables, unused_mut)] +//todo!() mod assets; pub mod languages; @@ -7,18 +7,54 @@ mod only_instance; mod open_listener; pub use assets::*; +use collections::VecDeque; +use editor::{Editor, MultiBuffer}; use gpui::{ - point, px, AppContext, AsyncWindowContext, Task, TitlebarOptions, WeakView, WindowBounds, - WindowKind, WindowOptions, + actions, point, px, AppContext, AsyncWindowContext, Context, PromptLevel, Task, + TitlebarOptions, ViewContext, VisualContext, WeakView, WindowBounds, WindowKind, WindowOptions, }; pub use only_instance::*; pub use open_listener::*; -use anyhow::Result; -use settings::Settings; -use std::sync::Arc; +use anyhow::{anyhow, Context as _, Result}; +use settings::{initial_local_settings_content, Settings}; +use std::{borrow::Cow, ops::Deref, sync::Arc}; +use util::{ + asset_str, + channel::ReleaseChannel, + paths::{self, LOCAL_SETTINGS_RELATIVE_PATH}, + ResultExt, +}; use uuid::Uuid; -use workspace::{AppState, Workspace}; +use workspace::{ + create_and_open_local_file, notifications::simple_message_notification::MessageNotification, + open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, +}; +use zed_actions::{OpenBrowser, OpenZedURL}; + +actions!( + About, + DebugElements, + DecreaseBufferFontSize, + Hide, + HideOthers, + IncreaseBufferFontSize, + Minimize, + OpenDefaultKeymap, + OpenDefaultSettings, + OpenKeymap, + OpenLicenses, + OpenLocalSettings, + OpenLog, + OpenSettings, + OpenTelemetryLog, + Quit, + ResetBufferFontSize, + ResetDatabase, + ShowAll, + ToggleFullScreen, + Zoom, +); pub fn build_window_options( bounds: Option, @@ -48,209 +84,206 @@ pub fn build_window_options( } } -pub fn init_zed_actions(cx: &mut AppContext) { - cx.observe_new_views(|workspace: &mut Workspace, cx| { +pub fn init_zed_actions(app_state: Arc, cx: &mut AppContext) { + cx.observe_new_views(move |workspace: &mut Workspace, _cx| { workspace - // cx.add_action(about); - // cx.add_global_action(|_: &Hide, cx: &mut gpui::AppContext| { - // cx.platform().hide(); + .register_action(about) + .register_action(|_, _: &Hide, cx| { + cx.hide(); + }) + .register_action(|_, _: &HideOthers, cx| { + cx.hide_other_apps(); + }) + .register_action(|_, _: &ShowAll, cx| { + cx.unhide_other_apps(); + }) + .register_action(|_, _: &Minimize, cx| { + cx.minimize_window(); + }) + .register_action(|_, _: &Zoom, cx| { + cx.zoom_window(); + }) + .register_action(|_, _: &ToggleFullScreen, cx| { + cx.toggle_full_screen(); + }) + .register_action(quit) + .register_action(|_, action: &OpenZedURL, cx| { + cx.global::>() + .open_urls(&[action.url.clone()]) + }) + .register_action(|_, action: &OpenBrowser, cx| cx.open_url(&action.url)) + //todo!(buffer font size) + // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size += 1.0) // }); - // cx.add_global_action(|_: &HideOthers, cx: &mut gpui::AppContext| { - // cx.platform().hide_other_apps(); + // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { + // theme::adjust_font_size(cx, |size| *size -= 1.0) // }); - // cx.add_global_action(|_: &ShowAll, cx: &mut gpui::AppContext| { - // cx.platform().unhide_other_apps(); + // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); + .register_action(|_, _: &install_cli::Install, cx| { + cx.spawn(|_, cx| async move { + install_cli::install_cli(cx.deref()) + .await + .context("error creating CLI symlink") + }) + .detach_and_log_err(cx); + }) + .register_action(|workspace, _: &OpenLog, cx| { + open_log_file(workspace, cx); + }) + .register_action(|workspace, _: &OpenLicenses, cx| { + open_bundled_file( + workspace, + asset_str::("licenses.md"), + "Open Source License Attribution", + "Markdown", + cx, + ); + }) + .register_action( + move |workspace: &mut Workspace, + _: &OpenTelemetryLog, + cx: &mut ViewContext| { + open_telemetry_log_file(workspace, cx); + }, + ) + .register_action( + move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { + create_and_open_local_file(&paths::KEYMAP, cx, Default::default) + .detach_and_log_err(cx); + }, + ) + .register_action( + move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { + create_and_open_local_file(&paths::SETTINGS, cx, || { + settings::initial_user_settings_content().as_ref().into() + }) + .detach_and_log_err(cx); + }, + ) + .register_action(open_local_settings_file) + .register_action( + move |workspace: &mut Workspace, + _: &OpenDefaultKeymap, + cx: &mut ViewContext| { + open_bundled_file( + workspace, + settings::default_keymap(), + "Default Key Bindings", + "JSON", + cx, + ); + }, + ) + .register_action( + move |workspace: &mut Workspace, + _: &OpenDefaultSettings, + cx: &mut ViewContext| { + open_bundled_file( + workspace, + settings::default_settings(), + "Default Settings", + "JSON", + cx, + ); + }, + ) + //todo!() + // cx.add_action({ + // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { + // let app_state = workspace.app_state().clone(); + // let markdown = app_state.languages.language_for_name("JSON"); + // let window = cx.window(); + // cx.spawn(|workspace, mut cx| async move { + // let markdown = markdown.await.log_err(); + // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { + // anyhow!("could not debug elements for window {}", window.id()) + // })?) + // .unwrap(); + // workspace + // .update(&mut cx, |workspace, cx| { + // workspace.with_local_workspace(cx, move |workspace, cx| { + // let project = workspace.project().clone(); + // let buffer = project + // .update(cx, |project, cx| { + // project.create_buffer(&content, markdown, cx) + // }) + // .expect("creating buffers on a local workspace always succeeds"); + // let buffer = cx.add_model(|cx| { + // MultiBuffer::singleton(buffer, cx) + // .with_title("Debug Elements".into()) + // }); + // workspace.add_item( + // Box::new(cx.add_view(|cx| { + // Editor::for_multibuffer(buffer, Some(project.clone()), cx) + // })), + // cx, + // ); + // }) + // })? + // .await + // }) + // .detach_and_log_err(cx); + // } // }); - // cx.add_action( - // |_: &mut Workspace, _: &Minimize, cx: &mut ViewContext| { - // cx.minimize_window(); + // .register_action( + // |workspace: &mut Workspace, + // _: &project_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); // }, // ); // cx.add_action( - // |_: &mut Workspace, _: &Zoom, cx: &mut ViewContext| { - // cx.zoom_window(); + // |workspace: &mut Workspace, + // _: &collab_ui::collab_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); // }, // ); // cx.add_action( - // |_: &mut Workspace, _: &ToggleFullScreen, cx: &mut ViewContext| { - // cx.toggle_full_screen(); + // |workspace: &mut Workspace, + // _: &collab_ui::chat_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); // }, // ); - .register_action(|workspace, _: &zed_actions::Quit, cx| quit(cx)); - // cx.add_global_action(move |action: &OpenZedURL, cx| { - // cx.global::>() - // .open_urls(vec![action.url.clone()]) - // }); - // cx.add_global_action(move |action: &OpenBrowser, cx| cx.platform().open_url(&action.url)); - // cx.add_global_action(move |_: &IncreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size += 1.0) - // }); - // cx.add_global_action(move |_: &DecreaseBufferFontSize, cx| { - // theme::adjust_font_size(cx, |size| *size -= 1.0) - // }); - // cx.add_global_action(move |_: &ResetBufferFontSize, cx| theme::reset_font_size(cx)); - // cx.add_global_action(move |_: &install_cli::Install, cx| { - // cx.spawn(|cx| async move { - // install_cli::install_cli(&cx) - // .await - // .context("error creating CLI symlink") - // }) - // .detach_and_log_err(cx); - // }); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenLog, cx: &mut ViewContext| { - // open_log_file(workspace, cx); - // }, - // ); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenLicenses, cx: &mut ViewContext| { - // open_bundled_file( - // workspace, - // asset_str::("licenses.md"), - // "Open Source License Attribution", - // "Markdown", - // cx, - // ); - // }, - // ); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenTelemetryLog, cx: &mut ViewContext| { - // open_telemetry_log_file(workspace, cx); - // }, - // ); - // cx.add_action( - // move |_: &mut Workspace, _: &OpenKeymap, cx: &mut ViewContext| { - // create_and_open_local_file(&paths::KEYMAP, cx, Default::default).detach_and_log_err(cx); - // }, - // ); - // cx.add_action( - // move |_: &mut Workspace, _: &OpenSettings, cx: &mut ViewContext| { - // create_and_open_local_file(&paths::SETTINGS, cx, || { - // settings::initial_user_settings_content().as_ref().into() - // }) - // .detach_and_log_err(cx); - // }, - // ); - // cx.add_action(open_local_settings_file); - // cx.add_action( - // move |workspace: &mut Workspace, _: &OpenDefaultKeymap, cx: &mut ViewContext| { - // open_bundled_file( - // workspace, - // settings::default_keymap(), - // "Default Key Bindings", - // "JSON", - // cx, - // ); - // }, - // ); - // cx.add_action( - // move |workspace: &mut Workspace, - // _: &OpenDefaultSettings, - // cx: &mut ViewContext| { - // open_bundled_file( - // workspace, - // settings::default_settings(), - // "Default Settings", - // "JSON", - // cx, - // ); - // }, - // ); - // cx.add_action({ - // move |workspace: &mut Workspace, _: &DebugElements, cx: &mut ViewContext| { - // let app_state = workspace.app_state().clone(); - // let markdown = app_state.languages.language_for_name("JSON"); - // let window = cx.window(); - // cx.spawn(|workspace, mut cx| async move { - // let markdown = markdown.await.log_err(); - // let content = to_string_pretty(&window.debug_elements(&cx).ok_or_else(|| { - // anyhow!("could not debug elements for window {}", window.id()) - // })?) - // .unwrap(); - // workspace - // .update(&mut cx, |workspace, cx| { - // workspace.with_local_workspace(cx, move |workspace, cx| { - // let project = workspace.project().clone(); - - // let buffer = project - // .update(cx, |project, cx| { - // project.create_buffer(&content, markdown, cx) - // }) - // .expect("creating buffers on a local workspace always succeeds"); - // let buffer = cx.add_model(|cx| { - // MultiBuffer::singleton(buffer, cx) - // .with_title("Debug Elements".into()) - // }); - // workspace.add_item( - // Box::new(cx.add_view(|cx| { - // Editor::for_multibuffer(buffer, Some(project.clone()), cx) - // })), - // cx, - // ); - // }) - // })? - // .await - // }) - // .detach_and_log_err(cx); - // } - // }); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &project_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::collab_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::chat_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &collab_ui::notification_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_action( - // |workspace: &mut Workspace, - // _: &terminal_panel::ToggleFocus, - // cx: &mut ViewContext| { - // workspace.toggle_panel_focus::(cx); - // }, - // ); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &NewWindow, cx: &mut AppContext| { - // if let Some(app_state) = app_state.upgrade() { - // open_new(&app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // } - // } - // }); - // cx.add_global_action({ - // let app_state = Arc::downgrade(&app_state); - // move |_: &NewFile, cx: &mut AppContext| { - // if let Some(app_state) = app_state.upgrade() { - // open_new(&app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // } - // } - // }); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &collab_ui::notification_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + // cx.add_action( + // |workspace: &mut Workspace, + // _: &terminal_panel::ToggleFocus, + // cx: &mut ViewContext| { + // workspace.toggle_panel_focus::(cx); + // }, + // ); + .register_action({ + let app_state = Arc::downgrade(&app_state); + move |_, _: &NewWindow, cx| { + if let Some(app_state) = app_state.upgrade() { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } + }) + .register_action({ + let app_state = Arc::downgrade(&app_state); + move |_, _: &NewFile, cx| { + if let Some(app_state) = app_state.upgrade() { + open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } + }); + //todo!() // load_default_keymap(cx); }) .detach(); @@ -415,46 +448,58 @@ pub fn initialize_workspace( }) } -fn quit(cx: &mut gpui::AppContext) { - let should_confirm = workspace::WorkspaceSettings::get_global(cx).confirm_quit; - cx.spawn(|mut cx| async move { - // let mut workspace_windows = cx - // .windows() - // .into_iter() - // .filter_map(|window| window.downcast::()) - // .collect::>(); +fn about(_: &mut Workspace, _: &About, cx: &mut gpui::ViewContext) { + let app_name = cx.global::().display_name(); + let version = env!("CARGO_PKG_VERSION"); + let prompt = cx.prompt(PromptLevel::Info, &format!("{app_name} {version}"), &["OK"]); + cx.foreground_executor() + .spawn(async { + prompt.await.ok(); + }) + .detach(); +} - // // If multiple windows have unsaved changes, and need a save prompt, - // // prompt in the active window before switching to a different window. - // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); +fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { + let should_confirm = WorkspaceSettings::get_global(cx).confirm_quit; + cx.spawn(|_, mut cx| async move { + let mut workspace_windows = cx.update(|_, cx| { + cx.windows() + .into_iter() + .filter_map(|window| window.downcast::()) + .collect::>() + })?; - // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { - // let answer = window.prompt( - // PromptLevel::Info, - // "Are you sure you want to quit?", - // &["Quit", "Cancel"], - // &mut cx, - // ); + // // If multiple windows have unsaved changes, and need a save prompt, + // // prompt in the active window before switching to a different window. + // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); - // if let Some(mut answer) = answer { - // let answer = answer.next().await; - // if answer != Some(0) { - // return Ok(()); - // } + // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + // let answer = window.prompt( + // PromptLevel::Info, + // "Are you sure you want to quit?", + // &["Quit", "Cancel"], + // &mut cx, + // ); + + // if let Some(mut answer) = answer { + // let answer = answer.next().await; + // if answer != Some(0) { + // return Ok(()); // } // } + // } - // // If the user cancels any save prompt, then keep the app open. - // for window in workspace_windows { - // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { - // workspace.prepare_to_close(true, cx) - // }) { - // if !should_close.await? { - // return Ok(()); - // } + // // If the user cancels any save prompt, then keep the app open. + // for window in workspace_windows { + // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { + // workspace.prepare_to_close(true, cx) + // }) { + // if !should_close.await? { + // return Ok(()); // } // } - cx.update(|cx| { + // } + cx.update(|_, cx| { cx.quit(); })?; @@ -462,3 +507,211 @@ fn quit(cx: &mut gpui::AppContext) { }) .detach_and_log_err(cx); } + +fn open_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + const MAX_LINES: usize = 1000; + workspace + .with_local_workspace(cx, move |workspace, cx| { + let fs = workspace.app_state().fs.clone(); + cx.spawn(|workspace, mut cx| async move { + let (old_log, new_log) = + futures::join!(fs.load(&paths::OLD_LOG), fs.load(&paths::LOG)); + + let mut lines = VecDeque::with_capacity(MAX_LINES); + for line in old_log + .iter() + .flat_map(|log| log.lines()) + .chain(new_log.iter().flat_map(|log| log.lines())) + { + if lines.len() == MAX_LINES { + lines.pop_front(); + } + lines.push_back(line); + } + let log = lines + .into_iter() + .flat_map(|line| [line, "\n"]) + .collect::(); + + workspace + .update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| buffer.edit([(0..0, log)], None, cx)); + + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Log".into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_multibuffer(buffer, Some(project), cx) + })), + cx, + ); + }) + .log_err(); + }) + .detach(); + }) + .detach(); +} + +fn open_local_settings_file( + workspace: &mut Workspace, + _: &OpenLocalSettings, + cx: &mut ViewContext, +) { + let project = workspace.project().clone(); + let worktree = project + .read(cx) + .visible_worktrees(cx) + .find_map(|tree| tree.read(cx).root_entry()?.is_dir().then_some(tree)); + if let Some(worktree) = worktree { + let tree_id = worktree.read(cx).id(); + cx.spawn(|workspace, mut cx| async move { + let file_path = &*LOCAL_SETTINGS_RELATIVE_PATH; + + if let Some(dir_path) = file_path.parent() { + if worktree.update(&mut cx, |tree, _| tree.entry_for_path(dir_path).is_none())? { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, dir_path), true, cx) + })? + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + } + + if worktree.update(&mut cx, |tree, _| tree.entry_for_path(file_path).is_none())? { + project + .update(&mut cx, |project, cx| { + project.create_entry((tree_id, file_path), false, cx) + })? + .ok_or_else(|| anyhow!("worktree was removed"))? + .await?; + } + + let editor = workspace + .update(&mut cx, |workspace, cx| { + workspace.open_path((tree_id, file_path), None, true, cx) + })? + .await? + .downcast::() + .ok_or_else(|| anyhow!("unexpected item type"))?; + + editor + .downgrade() + .update(&mut cx, |editor, cx| { + if let Some(buffer) = editor.buffer().read(cx).as_singleton() { + if buffer.read(cx).is_empty() { + buffer.update(cx, |buffer, cx| { + buffer.edit([(0..0, initial_local_settings_content())], None, cx) + }); + } + } + }) + .ok(); + + anyhow::Ok(()) + }) + .detach(); + } else { + workspace.show_notification(0, cx, |cx| { + cx.build_view(|_| MessageNotification::new("This project has no folders open.")) + }) + } +} + +fn open_telemetry_log_file(workspace: &mut Workspace, cx: &mut ViewContext) { + workspace.with_local_workspace(cx, move |workspace, cx| { + let app_state = workspace.app_state().clone(); + cx.spawn(|workspace, mut cx| async move { + async fn fetch_log_string(app_state: &Arc) -> Option { + let path = app_state.client.telemetry().log_file_path()?; + app_state.fs.load(&path).await.log_err() + } + + let log = fetch_log_string(&app_state).await.unwrap_or_else(|| "// No data has been collected yet".to_string()); + + const MAX_TELEMETRY_LOG_LEN: usize = 5 * 1024 * 1024; + let mut start_offset = log.len().saturating_sub(MAX_TELEMETRY_LOG_LEN); + if let Some(newline_offset) = log[start_offset..].find('\n') { + start_offset += newline_offset + 1; + } + let log_suffix = &log[start_offset..]; + let json = app_state.languages.language_for_name("JSON").await.log_err(); + + workspace.update(&mut cx, |workspace, cx| { + let project = workspace.project().clone(); + let buffer = project + .update(cx, |project, cx| project.create_buffer("", None, cx)) + .expect("creating buffers on a local workspace always succeeds"); + buffer.update(cx, |buffer, cx| { + buffer.set_language(json, cx); + buffer.edit( + [( + 0..0, + concat!( + "// Zed collects anonymous usage data to help us understand how people are using the app.\n", + "// Telemetry can be disabled via the `settings.json` file.\n", + "// Here is the data that has been reported for the current session:\n", + "\n" + ), + )], + None, + cx, + ); + buffer.edit([(buffer.len()..buffer.len(), log_suffix)], None, cx); + }); + + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title("Telemetry Log".into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| Editor::for_multibuffer(buffer, Some(project), cx))), + cx, + ); + }).log_err()?; + + Some(()) + }) + .detach(); + }).detach(); +} + +fn open_bundled_file( + workspace: &mut Workspace, + text: Cow<'static, str>, + title: &'static str, + language: &'static str, + cx: &mut ViewContext, +) { + let language = workspace.app_state().languages.language_for_name(language); + cx.spawn(|workspace, mut cx| async move { + let language = language.await.log_err(); + workspace + .update(&mut cx, |workspace, cx| { + workspace.with_local_workspace(cx, |workspace, cx| { + let project = workspace.project(); + let buffer = project.update(cx, move |project, cx| { + project + .create_buffer(text.as_ref(), language, cx) + .expect("creating buffers on a local workspace always succeeds") + }); + let buffer = cx.build_model(|cx| { + MultiBuffer::singleton(buffer, cx).with_title(title.into()) + }); + workspace.add_item( + Box::new(cx.build_view(|cx| { + Editor::for_multibuffer(buffer, Some(project.clone()), cx) + })), + cx, + ); + }) + })? + .await + }) + .detach_and_log_err(cx); +} diff --git a/crates/zed_actions2/src/lib.rs b/crates/zed_actions2/src/lib.rs index 097766492f..7f0c19853e 100644 --- a/crates/zed_actions2/src/lib.rs +++ b/crates/zed_actions2/src/lib.rs @@ -1,4 +1,4 @@ -use gpui::{action, actions}; +use gpui::action; // If the zed binary doesn't use anything in this crate, it will be optimized away // and the actions won't initialize. So we just provide an empty initialization function @@ -9,34 +9,11 @@ use gpui::{action, actions}; // https://github.com/mmastrac/rust-ctor/issues/280 pub fn init() {} -actions!( - About, - DebugElements, - DecreaseBufferFontSize, - Hide, - HideOthers, - IncreaseBufferFontSize, - Minimize, - OpenDefaultKeymap, - OpenDefaultSettings, - OpenKeymap, - OpenLicenses, - OpenLocalSettings, - OpenLog, - OpenSettings, - OpenTelemetryLog, - Quit, - ResetBufferFontSize, - ResetDatabase, - ShowAll, - ToggleFullScreen, - Zoom, -); - #[action] pub struct OpenBrowser { pub url: String, } + #[action] pub struct OpenZedURL { pub url: String, From 8aa076692ea2ad01eb9bf9a461b913e4882426fd Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 00:09:42 -0800 Subject: [PATCH 08/21] Add missing global --- crates/zed2/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 730dd255f9..b3797bed2a 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -113,6 +113,8 @@ fn main() { app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); + cx.set_global(listener.clone()); + load_embedded_fonts(cx); let mut store = SettingsStore::default(); From fc5ec47cc8bce50fc719b0638763a72d12148ea6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Fri, 10 Nov 2023 17:16:33 +0100 Subject: [PATCH 09/21] WIP --- crates/editor2/src/display_map/block_map.rs | 13 +- crates/editor2/src/element.rs | 436 +++++++++----------- crates/ui2/src/components/icon_button.rs | 16 +- 3 files changed, 218 insertions(+), 247 deletions(-) diff --git a/crates/editor2/src/display_map/block_map.rs b/crates/editor2/src/display_map/block_map.rs index aa5ff0e3d2..2f65903f08 100644 --- a/crates/editor2/src/display_map/block_map.rs +++ b/crates/editor2/src/display_map/block_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{Anchor, Editor, ExcerptId, ExcerptRange, ToPoint as _}; use collections::{Bound, HashMap, HashSet}; -use gpui::{AnyElement, ViewContext}; +use gpui::{AnyElement, Pixels, ViewContext}; use language::{BufferSnapshot, Chunk, Patch, Point}; use parking_lot::Mutex; use std::{ @@ -82,12 +82,11 @@ pub enum BlockStyle { pub struct BlockContext<'a, 'b> { pub view_context: &'b mut ViewContext<'a, Editor>, - pub anchor_x: f32, - pub scroll_x: f32, - pub gutter_width: f32, - pub gutter_padding: f32, - pub em_width: f32, - pub line_height: f32, + pub anchor_x: Pixels, + pub gutter_width: Pixels, + pub gutter_padding: Pixels, + pub em_width: Pixels, + pub line_height: Pixels, pub block_id: usize, } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 65f6edb18d..8a1385cf93 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -1,5 +1,8 @@ use crate::{ - display_map::{BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint}, + display_map::{ + BlockContext, BlockStyle, DisplaySnapshot, FoldStatus, HighlightedChunk, ToDisplayPoint, + TransformBlock, + }, editor_settings::ShowScrollbar, git::{diff_hunk_to_display, DisplayDiffHunk}, hover_popover::hover_at, @@ -15,17 +18,18 @@ use crate::{ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ - black, hsla, point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, - BorrowAppContext, BorrowWindow, Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, - ElementId, ElementInputHandler, Entity, FocusHandle, GlobalElementId, Hsla, InputHandler, - KeyContext, KeyDownEvent, KeyMatch, Line, LineLayout, Modifiers, MouseButton, MouseDownEvent, - MouseMoveEvent, MouseUpEvent, Pixels, ScrollWheelEvent, ShapedGlyph, Size, Style, TextRun, - TextStyle, TextSystem, ViewContext, WindowContext, WrappedLineLayout, + point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, + Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, + Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, + Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; use multi_buffer::Anchor; -use project::project_settings::{GitGutterSetting, ProjectSettings}; +use project::{ + project_settings::{GitGutterSetting, ProjectSettings}, + ProjectPath, +}; use settings::Settings; use smallvec::SmallVec; use std::{ @@ -39,6 +43,7 @@ use std::{ }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; +use ui::{h_stack, IconButton}; use util::ResultExt; use workspace::item::Item; @@ -1741,22 +1746,22 @@ impl EditorElement { .unwrap() .width; let scroll_width = longest_line_width.max(max_visible_line_width) + overscroll.width; - // todo!("blocks") - // let (scroll_width, blocks) = self.layout_blocks( - // start_row..end_row, - // &snapshot, - // size.x, - // scroll_width, - // gutter_padding, - // gutter_width, - // em_width, - // gutter_width + gutter_margin, - // line_height, - // &style, - // &line_layouts, - // editor, - // cx, - // ); + + let (scroll_width, blocks) = self.layout_blocks( + start_row..end_row, + &snapshot, + bounds.size.width, + scroll_width, + gutter_padding, + gutter_width, + em_width, + gutter_width + gutter_margin, + line_height, + &style, + &line_layouts, + editor, + cx, + ); let scroll_max = point( f32::from((scroll_width - text_size.width) / em_width).max(0.0), @@ -1948,226 +1953,181 @@ impl EditorElement { } } - // #[allow(clippy::too_many_arguments)] - // fn layout_blocks( - // &mut self, - // rows: Range, - // snapshot: &EditorSnapshot, - // editor_width: f32, - // scroll_width: f32, - // gutter_padding: f32, - // gutter_width: f32, - // em_width: f32, - // text_x: f32, - // line_height: f32, - // style: &EditorStyle, - // line_layouts: &[LineWithInvisibles], - // editor: &mut Editor, - // cx: &mut ViewContext, - // ) -> (f32, Vec) { - // let mut block_id = 0; - // let scroll_x = snapshot.scroll_anchor.offset.x; - // let (fixed_blocks, non_fixed_blocks) = snapshot - // .blocks_in_range(rows.clone()) - // .partition::, _>(|(_, block)| match block { - // TransformBlock::ExcerptHeader { .. } => false, - // TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, - // }); - // let mut render_block = |block: &TransformBlock, width: f32, block_id: usize| { - // let mut element = match block { - // TransformBlock::Custom(block) => { - // let align_to = block - // .position() - // .to_point(&snapshot.buffer_snapshot) - // .to_display_point(snapshot); - // let anchor_x = text_x - // + if rows.contains(&align_to.row()) { - // line_layouts[(align_to.row() - rows.start) as usize] - // .line - // .x_for_index(align_to.column() as usize) - // } else { - // layout_line(align_to.row(), snapshot, style, cx.text_layout_cache()) - // .x_for_index(align_to.column() as usize) - // }; + #[allow(clippy::too_many_arguments)] + fn layout_blocks( + &mut self, + rows: Range, + snapshot: &EditorSnapshot, + editor_width: Pixels, + scroll_width: Pixels, + gutter_padding: Pixels, + gutter_width: Pixels, + em_width: Pixels, + text_x: Pixels, + line_height: Pixels, + style: &EditorStyle, + line_layouts: &[LineWithInvisibles], + editor: &mut Editor, + cx: &mut ViewContext, + ) -> (Pixels, Vec) { + let mut block_id = 0; + let scroll_x = snapshot.scroll_anchor.offset.x; + let (fixed_blocks, non_fixed_blocks) = snapshot + .blocks_in_range(rows.clone()) + .partition::, _>(|(_, block)| match block { + TransformBlock::ExcerptHeader { .. } => false, + TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, + }); + let mut render_block = |block: &TransformBlock, width: Pixels, block_id: usize| { + let mut element = match block { + TransformBlock::Custom(block) => { + let align_to = block + .position() + .to_point(&snapshot.buffer_snapshot) + .to_display_point(snapshot); + let anchor_x = text_x + + if rows.contains(&align_to.row()) { + line_layouts[(align_to.row() - rows.start) as usize] + .line + .x_for_index(align_to.column() as usize) + } else { + layout_line(align_to.row(), snapshot, style, cx) + .unwrap() + .x_for_index(align_to.column() as usize) + }; - // block.render(&mut BlockContext { - // view_context: cx, - // anchor_x, - // gutter_padding, - // line_height, - // scroll_x, - // gutter_width, - // em_width, - // block_id, - // }) - // } - // TransformBlock::ExcerptHeader { - // id, - // buffer, - // range, - // starts_new_buffer, - // .. - // } => { - // let tooltip_style = theme::current(cx).tooltip.clone(); - // let include_root = editor - // .project - // .as_ref() - // .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) - // .unwrap_or_default(); - // let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { - // let jump_path = ProjectPath { - // worktree_id: file.worktree_id(cx), - // path: file.path.clone(), - // }; - // let jump_anchor = range - // .primary - // .as_ref() - // .map_or(range.context.start, |primary| primary.start); - // let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); + block.render(&mut BlockContext { + view_context: cx, + anchor_x, + gutter_padding, + line_height, + // scroll_x, + gutter_width, + em_width, + block_id, + }) + } + TransformBlock::ExcerptHeader { + id, + buffer, + range, + starts_new_buffer, + .. + } => { + let include_root = editor + .project + .as_ref() + .map(|project| project.read(cx).visible_worktrees(cx).count() > 1) + .unwrap_or_default(); + let jump_icon = project::File::from_dyn(buffer.file()).map(|file| { + let jump_path = ProjectPath { + worktree_id: file.worktree_id(cx), + path: file.path.clone(), + }; + let jump_anchor = range + .primary + .as_ref() + .map_or(range.context.start, |primary| primary.start); + let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); - // enum JumpIcon {} - // MouseEventHandler::new::((*id).into(), cx, |state, _| { - // let style = style.jump_icon.style_for(state); - // Svg::new("icons/arrow_up_right.svg") - // .with_color(style.color) - // .constrained() - // .with_width(style.icon_width) - // .aligned() - // .contained() - // .with_style(style.container) - // .constrained() - // .with_width(style.button_width) - // .with_height(style.button_width) - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, editor, cx| { - // if let Some(workspace) = editor - // .workspace - // .as_ref() - // .and_then(|(workspace, _)| workspace.upgrade(cx)) - // { - // workspace.update(cx, |workspace, cx| { - // Editor::jump( - // workspace, - // jump_path.clone(), - // jump_position, - // jump_anchor, - // cx, - // ); - // }); - // } - // }) - // .with_tooltip::( - // (*id).into(), - // "Jump to Buffer".to_string(), - // Some(Box::new(crate::OpenExcerpts)), - // tooltip_style.clone(), - // cx, - // ) - // .aligned() - // .flex_float() - // }); + // todo!("avoid ElementId collision risk here") + IconButton::new(usize::from(*id), ui::Icon::ArrowUpRight) + .on_click(move |editor, cx| { + if let Some(workspace) = editor + .workspace + .as_ref() + .and_then(|(workspace, _)| workspace.upgrade(cx)) + { + workspace.update(cx, |workspace, cx| { + Editor::jump( + workspace, + jump_path.clone(), + jump_position, + jump_anchor, + cx, + ); + }); + } + }) + .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding) + }); - // if *starts_new_buffer { - // let editor_font_size = style.text.font_size; - // let style = &style.diagnostic_path_header; - // let font_size = (style.text_scale_factor * editor_font_size).round(); + if *starts_new_buffer { + let path = buffer.resolve_file_path(cx, include_root); + let mut filename = None; + let mut parent_path = None; + // Can't use .and_then() because `.file_name()` and `.parent()` return references :( + if let Some(path) = path { + filename = path.file_name().map(|f| f.to_string_lossy().to_string()); + parent_path = + path.parent().map(|p| p.to_string_lossy().to_string() + "/"); + } - // let path = buffer.resolve_file_path(cx, include_root); - // let mut filename = None; - // let mut parent_path = None; - // // Can't use .and_then() because `.file_name()` and `.parent()` return references :( - // if let Some(path) = path { - // filename = path.file_name().map(|f| f.to_string_lossy.to_string()); - // parent_path = - // path.parent().map(|p| p.to_string_lossy.to_string() + "/"); - // } + h_stack() + .child(filename.unwrap_or_else(|| "untitled".to_string())) + .children(parent_path) + .children(jump_icon) + .p_x(gutter_padding) + } else { + let text_style = style.text.clone(); + h_stack() + .child("⋯") + .children(jump_icon) + .p_x(gutter_padding) + .expanded() + .into_any_named("collapsed context") + } + } + }; - // Flex::row() - // .with_child( - // Label::new( - // filename.unwrap_or_else(|| "untitled".to_string()), - // style.filename.text.clone().with_font_size(font_size), - // ) - // .contained() - // .with_style(style.filename.container) - // .aligned(), - // ) - // .with_children(parent_path.map(|path| { - // Label::new(path, style.path.text.clone().with_font_size(font_size)) - // .contained() - // .with_style(style.path.container) - // .aligned() - // })) - // .with_children(jump_icon) - // .contained() - // .with_style(style.container) - // .with_padding_left(gutter_padding) - // .with_padding_right(gutter_padding) - // .expanded() - // .into_any_named("path header block") - // } else { - // let text_style = style.text.clone(); - // Flex::row() - // .with_child(Label::new("⋯", text_style)) - // .with_children(jump_icon) - // .contained() - // .with_padding_left(gutter_padding) - // .with_padding_right(gutter_padding) - // .expanded() - // .into_any_named("collapsed context") - // } - // } - // }; + // element.layout( + // SizeConstraint { + // min: gpui::Point::::zero(), + // max: point(width, block.height() as f32 * line_height), + // }, + // editor, + // cx, + // ); + element + }; - // element.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point(width, block.height() as f32 * line_height), - // }, - // editor, - // cx, - // ); - // element - // }; - - // let mut fixed_block_max_width = 0f32; - // let mut blocks = Vec::new(); - // for (row, block) in fixed_blocks { - // let element = render_block(block, f32::INFINITY, block_id); - // block_id += 1; - // fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width); - // blocks.push(BlockLayout { - // row, - // element, - // style: BlockStyle::Fixed, - // }); - // } - // for (row, block) in non_fixed_blocks { - // let style = match block { - // TransformBlock::Custom(block) => block.style(), - // TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, - // }; - // let width = match style { - // BlockStyle::Sticky => editor_width, - // BlockStyle::Flex => editor_width - // .max(fixed_block_max_width) - // .max(gutter_width + scroll_width), - // BlockStyle::Fixed => unreachable!(), - // }; - // let element = render_block(block, width, block_id); - // block_id += 1; - // blocks.push(BlockLayout { - // row, - // element, - // style, - // }); - // } - // ( - // scroll_width.max(fixed_block_max_width - gutter_width), - // blocks, - // ) - // } + let mut fixed_block_max_width = Pixels::ZERO; + let mut blocks = Vec::new(); + for (row, block) in fixed_blocks { + let element = render_block(block, f32::INFINITY, block_id); + block_id += 1; + fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width); + blocks.push(BlockLayout { + row, + element, + style: BlockStyle::Fixed, + }); + } + for (row, block) in non_fixed_blocks { + let style = match block { + TransformBlock::Custom(block) => block.style(), + TransformBlock::ExcerptHeader { .. } => BlockStyle::Sticky, + }; + let width = match style { + BlockStyle::Sticky => editor_width, + BlockStyle::Flex => editor_width + .max(fixed_block_max_width) + .max(gutter_width + scroll_width), + BlockStyle::Fixed => unreachable!(), + }; + let element = render_block(block, width, block_id); + block_id += 1; + blocks.push(BlockLayout { + row, + element, + style, + }); + } + ( + scroll_width.max(fixed_block_max_width - gutter_width), + blocks, + ) + } fn paint_mouse_listeners( &mut self, diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 91653ea8cd..f093804aa8 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,8 +1,8 @@ use std::sync::Arc; -use gpui::{rems, MouseButton}; +use gpui::{rems, MouseButton, VisualContext}; -use crate::{h_stack, prelude::*}; +use crate::{h_stack, prelude::*, TextTooltip}; use crate::{ClickHandler, Icon, IconColor, IconElement}; struct IconButtonHandlers { @@ -22,6 +22,7 @@ pub struct IconButton { color: IconColor, variant: ButtonVariant, state: InteractionState, + tooltip: Option, handlers: IconButtonHandlers, } @@ -33,6 +34,7 @@ impl IconButton { color: IconColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), + tooltip: None, handlers: IconButtonHandlers::default(), } } @@ -57,6 +59,11 @@ impl IconButton { self } + pub fn tooltip(mut self, tooltip: impl Into) -> Self { + self.tooltip = Some(tooltip.into()); + self + } + pub fn on_click( mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext) + Send + Sync, @@ -103,6 +110,11 @@ impl IconButton { }); } + if let Some(tooltip) = self.tooltip.clone() { + button = + button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone()))); + } + button } } From f9b9b7549f6f5d8b34d10b4c61975848d1e78f7b Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 16:03:06 +0100 Subject: [PATCH 10/21] Render block elements Co-Authored-By: Julia --- crates/editor2/src/editor.rs | 139 ++++++++++++--------------- crates/editor2/src/element.rs | 136 +++++++++++++------------- crates/ui2/src/components/tooltip.rs | 6 +- 3 files changed, 132 insertions(+), 149 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index fe98dd8679..22ceea51a3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -22,7 +22,7 @@ mod editor_tests; pub mod test; use ::git::diff::DiffHunk; use aho_corasick::AhoCorasick; -use anyhow::{Context as _, Result}; +use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; use client::{ClickhouseEvent, Client, Collaborator, ParticipantIndex, TelemetrySettings}; use clock::ReplicaId; @@ -43,8 +43,8 @@ use gpui::{ AsyncWindowContext, BackgroundExecutor, Bounds, ClipboardItem, Component, Context, EventEmitter, FocusHandle, FontFeatures, FontStyle, FontWeight, HighlightStyle, Hsla, InputHandler, KeyContext, Model, MouseButton, ParentElement, Pixels, Render, - StatelessInteractive, Styled, Subscription, Task, TextStyle, UniformListScrollHandle, View, - ViewContext, VisualContext, WeakView, WindowContext, + StatefulInteractive, StatelessInteractive, Styled, Subscription, Task, TextStyle, + UniformListScrollHandle, View, ViewContext, VisualContext, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; @@ -69,7 +69,7 @@ pub use multi_buffer::{ }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; -use project::{FormatTrigger, Location, Project, ProjectTransaction}; +use project::{FormatTrigger, Location, Project, ProjectPath, ProjectTransaction}; use rand::prelude::*; use rpc::proto::*; use scroll::{ @@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{IconButton, StyledExt}; +use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::ItemEvent, searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, @@ -8869,46 +8869,50 @@ impl Editor { // }); // } - // fn jump( - // workspace: &mut Workspace, - // path: ProjectPath, - // position: Point, - // anchor: language::Anchor, - // cx: &mut ViewContext, - // ) { - // let editor = workspace.open_path(path, None, true, cx); - // cx.spawn(|_, mut cx| async move { - // let editor = editor - // .await? - // .downcast::() - // .ok_or_else(|| anyhow!("opened item was not an editor"))? - // .downgrade(); - // editor.update(&mut cx, |editor, cx| { - // let buffer = editor - // .buffer() - // .read(cx) - // .as_singleton() - // .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?; - // let buffer = buffer.read(cx); - // let cursor = if buffer.can_resolve(&anchor) { - // language::ToPoint::to_point(&anchor, buffer) - // } else { - // buffer.clip_point(position, Bias::Left) - // }; + fn jump( + &mut self, + path: ProjectPath, + position: Point, + anchor: language::Anchor, + cx: &mut ViewContext, + ) { + let workspace = self.workspace(); + cx.spawn(|_, mut cx| async move { + let workspace = workspace.ok_or_else(|| anyhow!("cannot jump without workspace"))?; + let editor = workspace.update(&mut cx, |workspace, cx| { + workspace.open_path(path, None, true, cx) + })?; + let editor = editor + .await? + .downcast::() + .ok_or_else(|| anyhow!("opened item was not an editor"))? + .downgrade(); + editor.update(&mut cx, |editor, cx| { + let buffer = editor + .buffer() + .read(cx) + .as_singleton() + .ok_or_else(|| anyhow!("cannot jump in a multi-buffer"))?; + let buffer = buffer.read(cx); + let cursor = if buffer.can_resolve(&anchor) { + language::ToPoint::to_point(&anchor, buffer) + } else { + buffer.clip_point(position, Bias::Left) + }; - // let nav_history = editor.nav_history.take(); - // editor.change_selections(Some(Autoscroll::newest()), cx, |s| { - // s.select_ranges([cursor..cursor]); - // }); - // editor.nav_history = nav_history; + let nav_history = editor.nav_history.take(); + editor.change_selections(Some(Autoscroll::newest()), cx, |s| { + s.select_ranges([cursor..cursor]); + }); + editor.nav_history = nav_history; - // anyhow::Ok(()) - // })??; + anyhow::Ok(()) + })??; - // anyhow::Ok(()) - // }) - // .detach_and_log_err(cx); - // } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } fn marked_text_ranges(&self, cx: &AppContext) -> Option>> { let snapshot = self.buffer.read(cx).read(cx); @@ -9973,43 +9977,20 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend } let message = diagnostic.message; Arc::new(move |cx: &mut BlockContext| { - todo!() - // let message = message.clone(); - // let settings = ThemeSettings::get_global(cx); - // let tooltip_style = settings.theme.tooltip.clone(); - // let theme = &settings.theme.editor; - // let style = diagnostic_style(diagnostic.severity, is_valid, theme); - // let font_size = (style.text_scale_factor * settings.buffer_font_size(cx)).round(); - // let anchor_x = cx.anchor_x; - // enum BlockContextToolip {} - // MouseEventHandler::new::(cx.block_id, cx, |_, _| { - // Flex::column() - // .with_children(highlighted_lines.iter().map(|(line, highlights)| { - // Label::new( - // line.clone(), - // style.message.clone().with_font_size(font_size), - // ) - // .with_highlights(highlights.clone()) - // .contained() - // .with_margin_left(anchor_x) - // })) - // .aligned() - // .left() - // .into_any() - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .on_click(MouseButton::Left, move |_, _, cx| { - // cx.write_to_clipboard(ClipboardItem::new(message.clone())); - // }) - // // We really need to rethink this ID system... - // .with_tooltip::( - // cx.block_id, - // "Copy diagnostic message", - // None, - // tooltip_style, - // cx, - // ) - // .into_any() + let message = message.clone(); + v_stack() + .id(cx.block_id) + .children(highlighted_lines.iter().map(|(line, highlights)| { + div() + .child(HighlightedLabel::new(line.clone(), highlights.clone())) + .ml(cx.anchor_x) + })) + .cursor_pointer() + .on_click(move |_, _, cx| { + cx.write_to_clipboard(ClipboardItem::new(message.clone())); + }) + .tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message"))) + .render() }) } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 8a1385cf93..64a281d9e2 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -19,9 +19,10 @@ use anyhow::Result; use collections::{BTreeMap, HashMap}; use gpui::{ point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, - Bounds, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, - Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, - Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, ViewContext, WindowContext, + Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, + ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, + MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, + ViewContext, WindowContext, }; use itertools::Itertools; use language::language_settings::ShowWhitespaceSetting; @@ -1176,30 +1177,31 @@ impl EditorElement { } } - // fn paint_blocks( - // &mut self, - // bounds: Bounds, - // visible_bounds: Bounds, - // layout: &mut LayoutState, - // editor: &mut Editor, - // cx: &mut ViewContext, - // ) { - // let scroll_position = layout.position_map.snapshot.scroll_position(); - // let scroll_left = scroll_position.x * layout.position_map.em_width; - // let scroll_top = scroll_position.y * layout.position_map.line_height; + fn paint_blocks( + &mut self, + bounds: Bounds, + layout: &mut LayoutState, + editor: &mut Editor, + cx: &mut ViewContext, + ) { + let scroll_position = layout.position_map.snapshot.scroll_position(); + let scroll_left = scroll_position.x * layout.position_map.em_width; + let scroll_top = scroll_position.y * layout.position_map.line_height; - // for block in &mut layout.blocks { - // let mut origin = bounds.origin - // + point( - // 0., - // block.row as f32 * layout.position_map.line_height - scroll_top, - // ); - // if !matches!(block.style, BlockStyle::Sticky) { - // origin += point(-scroll_left, 0.); - // } - // block.element.paint(origin, visible_bounds, editor, cx); - // } - // } + for block in &mut layout.blocks { + let mut origin = bounds.origin + + point( + Pixels::ZERO, + block.row as f32 * layout.position_map.line_height - scroll_top, + ); + if !matches!(block.style, BlockStyle::Sticky) { + origin += point(-scroll_left, Pixels::ZERO); + } + block + .element + .draw(origin, block.available_space, editor, cx); + } + } fn column_pixels(&self, column: usize, cx: &ViewContext) -> Pixels { let style = &self.style; @@ -1942,7 +1944,7 @@ impl EditorElement { fold_ranges, line_number_layouts, display_hunks, - // blocks, + blocks, selections, context_menu, code_actions_indicator, @@ -1978,7 +1980,11 @@ impl EditorElement { TransformBlock::ExcerptHeader { .. } => false, TransformBlock::Custom(block) => block.style() == BlockStyle::Fixed, }); - let mut render_block = |block: &TransformBlock, width: Pixels, block_id: usize| { + let mut render_block = |block: &TransformBlock, + available_space: Size, + block_id: usize, + editor: &mut Editor, + cx: &mut ViewContext| { let mut element = match block { TransformBlock::Custom(block) => { let align_to = block @@ -2031,28 +2037,15 @@ impl EditorElement { let jump_position = language::ToPoint::to_point(&jump_anchor, buffer); // todo!("avoid ElementId collision risk here") - IconButton::new(usize::from(*id), ui::Icon::ArrowUpRight) - .on_click(move |editor, cx| { - if let Some(workspace) = editor - .workspace - .as_ref() - .and_then(|(workspace, _)| workspace.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Editor::jump( - workspace, - jump_path.clone(), - jump_position, - jump_anchor, - cx, - ); - }); - } + let icon_button_id: usize = id.clone().into(); + IconButton::new(icon_button_id, ui::Icon::ArrowUpRight) + .on_click(move |editor: &mut Editor, cx| { + editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); }) .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding) }); - if *starts_new_buffer { + let element = if *starts_new_buffer { let path = buffer.resolve_file_path(cx, include_root); let mut filename = None; let mut parent_path = None; @@ -2066,40 +2059,34 @@ impl EditorElement { h_stack() .child(filename.unwrap_or_else(|| "untitled".to_string())) .children(parent_path) - .children(jump_icon) - .p_x(gutter_padding) + .children(jump_icon) // .p_x(gutter_padding) } else { let text_style = style.text.clone(); - h_stack() - .child("⋯") - .children(jump_icon) - .p_x(gutter_padding) - .expanded() - .into_any_named("collapsed context") - } + h_stack().child("⋯").children(jump_icon) // .p_x(gutter_padding) + }; + element.render() } }; - // element.layout( - // SizeConstraint { - // min: gpui::Point::::zero(), - // max: point(width, block.height() as f32 * line_height), - // }, - // editor, - // cx, - // ); - element + let size = element.measure(available_space, editor, cx); + (element, size) }; let mut fixed_block_max_width = Pixels::ZERO; let mut blocks = Vec::new(); for (row, block) in fixed_blocks { - let element = render_block(block, f32::INFINITY, block_id); + let available_space = size( + AvailableSpace::MinContent, + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + let (element, element_size) = + render_block(block, available_space, block_id, editor, cx); block_id += 1; - fixed_block_max_width = fixed_block_max_width.max(element.size().x + em_width); + fixed_block_max_width = fixed_block_max_width.max(element_size.width + em_width); blocks.push(BlockLayout { row, element, + available_space, style: BlockStyle::Fixed, }); } @@ -2115,11 +2102,16 @@ impl EditorElement { .max(gutter_width + scroll_width), BlockStyle::Fixed => unreachable!(), }; - let element = render_block(block, width, block_id); + let available_space = size( + AvailableSpace::Definite(width), + AvailableSpace::Definite(block.height() as f32 * line_height), + ); + let (element, _) = render_block(block, available_space, block_id, editor, cx); block_id += 1; blocks.push(BlockLayout { row, element, + available_space, style, }); } @@ -2630,11 +2622,18 @@ impl Element for EditorElement { &layout.position_map, cx, ); + self.paint_background(gutter_bounds, text_bounds, &layout, cx); if layout.gutter_size.width > Pixels::ZERO { self.paint_gutter(gutter_bounds, &mut layout, editor, cx); } + self.paint_text(text_bounds, &mut layout, editor, cx); + + if !layout.blocks.is_empty() { + self.paint_blocks(bounds, &mut layout, editor, cx); + } + let input_handler = ElementInputHandler::new(bounds, cx); cx.handle_input(&editor.focus_handle, input_handler); }); @@ -3255,7 +3254,7 @@ pub struct LayoutState { highlighted_rows: Option>, line_number_layouts: Vec>, display_hunks: Vec, - // blocks: Vec, + blocks: Vec, highlighted_ranges: Vec<(Range, Hsla)>, fold_ranges: Vec<(BufferRow, Range, Hsla)>, selections: Vec<(PlayerColor, Vec)>, @@ -3358,6 +3357,7 @@ impl PositionMap { struct BlockLayout { row: u32, element: AnyElement, + available_space: Size, style: BlockStyle, } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index e6c0e3f44d..ee3e9708c0 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -9,8 +9,10 @@ pub struct TextTooltip { } impl TextTooltip { - pub fn new(str: SharedString) -> Self { - Self { title: str } + pub fn new(title: impl Into) -> Self { + Self { + title: title.into(), + } } } From b6914bf0fda2e04415db3b2f4d26650e89a3b71d Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 16:09:29 +0100 Subject: [PATCH 11/21] Re-enable find all references Co-Authored-By: Julia --- crates/editor2/src/editor.rs | 84 ++++++++++++++++------------------- crates/editor2/src/element.rs | 6 ++- 2 files changed, 44 insertions(+), 46 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index 22ceea51a3..d02521fac1 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -7588,53 +7588,47 @@ impl Editor { }) } - // pub fn find_all_references( - // workspace: &mut Workspace, - // _: &FindAllReferences, - // cx: &mut ViewContext, - // ) -> Option>> { - // let active_item = workspace.active_item(cx)?; - // let editor_handle = active_item.act_as::(cx)?; + pub fn find_all_references( + &mut self, + _: &FindAllReferences, + cx: &mut ViewContext, + ) -> Option>> { + let buffer = self.buffer.read(cx); + let head = self.selections.newest::(cx).head(); + let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; + let replica_id = self.replica_id(cx); - // let editor = editor_handle.read(cx); - // let buffer = editor.buffer.read(cx); - // let head = editor.selections.newest::(cx).head(); - // let (buffer, head) = buffer.text_anchor_for_position(head, cx)?; - // let replica_id = editor.replica_id(cx); + let workspace = self.workspace()?; + let project = workspace.read(cx).project().clone(); + let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); + Some(cx.spawn(|_, mut cx| async move { + let locations = references.await?; + if locations.is_empty() { + return Ok(()); + } - // let project = workspace.project().clone(); - // let references = project.update(cx, |project, cx| project.references(&buffer, head, cx)); - // Some(cx.spawn_labeled( - // "Finding All References...", - // |workspace, mut cx| async move { - // let locations = references.await?; - // if locations.is_empty() { - // return Ok(()); - // } + workspace.update(&mut cx, |workspace, cx| { + let title = locations + .first() + .as_ref() + .map(|location| { + let buffer = location.buffer.read(cx); + format!( + "References to `{}`", + buffer + .text_for_range(location.range.clone()) + .collect::() + ) + }) + .unwrap(); + Self::open_locations_in_multibuffer( + workspace, locations, replica_id, title, false, cx, + ); + })?; - // workspace.update(&mut cx, |workspace, cx| { - // let title = locations - // .first() - // .as_ref() - // .map(|location| { - // let buffer = location.buffer.read(cx); - // format!( - // "References to `{}`", - // buffer - // .text_for_range(location.range.clone()) - // .collect::() - // ) - // }) - // .unwrap(); - // Self::open_locations_in_multibuffer( - // workspace, locations, replica_id, title, false, cx, - // ); - // })?; - - // Ok(()) - // }, - // )) - // } + Ok(()) + })) + } /// Opens a multibuffer with the given project locations in it pub fn open_locations_in_multibuffer( @@ -7685,7 +7679,7 @@ impl Editor { editor.update(cx, |editor, cx| { editor.highlight_background::( ranges_to_highlight, - |theme| todo!("theme.editor.highlighted_line_background"), + |theme| theme.editor_highlighted_line_background, cx, ); }); diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 64a281d9e2..38b54ea2b1 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -2565,7 +2565,11 @@ impl Element for EditorElement { }); // on_action(cx, Editor::rename); todo!() // on_action(cx, Editor::confirm_rename); todo!() - // on_action(cx, Editor::find_all_references); todo!() + register_action(cx, |editor, action, cx| { + editor + .find_all_references(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }); register_action(cx, Editor::next_copilot_suggestion); register_action(cx, Editor::previous_copilot_suggestion); register_action(cx, Editor::copilot_suggest); From d855e91e438006ef03f8f118a7eec0d0415cc465 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 16:38:20 +0100 Subject: [PATCH 12/21] Honor `cmd-w` to close active item Co-Authored-By: Julia --- crates/gpui2/src/action.rs | 6 +++++- crates/workspace2/src/pane.rs | 35 ++++++++++++++++++++--------------- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 06e93e275d..16487cf18a 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -68,8 +68,12 @@ where A: for<'a> Deserialize<'a> + PartialEq + Clone + Default + std::fmt::Debug + 'static, { fn qualified_name() -> SharedString { + let name = type_name::(); + let mut separator_matches = name.rmatch_indices("::"); + separator_matches.next().unwrap(); + let name_start_ix = separator_matches.next().map_or(0, |(ix, _)| ix + 2); // todo!() remove the 2 replacement when migration is done - type_name::().replace("2::", "::").into() + name[name_start_ix..].replace("2::", "::").into() } fn build(params: Option) -> Result> diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index d0613e13ab..e3ea4863c9 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -733,21 +733,21 @@ impl Pane { // self.activate_item(index, activate_pane, activate_pane, cx); // } - // pub fn close_active_item( - // &mut self, - // action: &CloseActiveItem, - // cx: &mut ViewContext, - // ) -> Option>> { - // if self.items.is_empty() { - // return None; - // } - // let active_item_id = self.items[self.active_item_index].id(); - // Some(self.close_item_by_id( - // active_item_id, - // action.save_intent.unwrap_or(SaveIntent::Close), - // cx, - // )) - // } + pub fn close_active_item( + &mut self, + action: &CloseActiveItem, + cx: &mut ViewContext, + ) -> Option>> { + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_item_by_id( + active_item_id, + action.save_intent.unwrap_or(SaveIntent::Close), + cx, + )) + } pub fn close_item_by_id( &mut self, @@ -1919,7 +1919,12 @@ impl Render for Pane { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { v_stack() + .context("Pane") .size_full() + .on_action(|pane: &mut Self, action, cx| { + pane.close_active_item(action, cx) + .map(|task| task.detach_and_log_err(cx)); + }) .child(self.render_tab_bar(cx)) .child(div() /* todo!(toolbar) */) .child(if let Some(item) = self.active_item() { From 0b8ec5372ba5679f02935c037bffbe623869a825 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 17:06:18 +0100 Subject: [PATCH 13/21] Return the line length when `x` is past the last glyph Co-Authored-By: Julia --- crates/gpui2/src/text_system/line_layout.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/gpui2/src/text_system/line_layout.rs b/crates/gpui2/src/text_system/line_layout.rs index db7140b040..7e9176caca 100644 --- a/crates/gpui2/src/text_system/line_layout.rs +++ b/crates/gpui2/src/text_system/line_layout.rs @@ -54,9 +54,9 @@ impl LineLayout { pub fn closest_index_for_x(&self, x: Pixels) -> usize { let mut prev_index = 0; let mut prev_x = px(0.); + for run in self.runs.iter() { for glyph in run.glyphs.iter() { - glyph.index; if glyph.position.x >= x { if glyph.position.x - x < x - prev_x { return glyph.index; @@ -68,7 +68,8 @@ impl LineLayout { prev_x = glyph.position.x; } } - prev_index + 1 + + self.len } pub fn x_for_index(&self, index: usize) -> Pixels { From 7f5014b34a29ec39099f222f125756caf97220e6 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Tue, 14 Nov 2023 17:15:33 +0100 Subject: [PATCH 14/21] Add red background to blocks that need styling --- crates/editor2/src/editor.rs | 2 ++ crates/editor2/src/element.rs | 10 ++++++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index d02521fac1..ebe78d95b3 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -9974,6 +9974,8 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend let message = message.clone(); v_stack() .id(cx.block_id) + .size_full() + .bg(gpui::red()) .children(highlighted_lines.iter().map(|(line, highlights)| { div() .child(HighlightedLabel::new(line.clone(), highlights.clone())) diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index 38b54ea2b1..638ed33891 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -21,7 +21,7 @@ use gpui::{ point, px, relative, size, transparent_black, Action, AnyElement, AvailableSpace, BorrowWindow, Bounds, Component, ContentMask, Corners, DispatchPhase, Edges, Element, ElementId, ElementInputHandler, Entity, Hsla, Line, MouseButton, MouseDownEvent, MouseMoveEvent, - MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, TextRun, TextStyle, + MouseUpEvent, ParentElement, Pixels, ScrollWheelEvent, Size, Style, Styled, TextRun, TextStyle, ViewContext, WindowContext, }; use itertools::Itertools; @@ -2057,12 +2057,18 @@ impl EditorElement { } h_stack() + .size_full() + .bg(gpui::red()) .child(filename.unwrap_or_else(|| "untitled".to_string())) .children(parent_path) .children(jump_icon) // .p_x(gutter_padding) } else { let text_style = style.text.clone(); - h_stack().child("⋯").children(jump_icon) // .p_x(gutter_padding) + h_stack() + .size_full() + .bg(gpui::red()) + .child("⋯") + .children(jump_icon) // .p_x(gutter_padding) }; element.render() } From 2c3c238c9d3974e2d95ebcc8545d6ba60fd645ed Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 14 Nov 2023 09:47:48 -0800 Subject: [PATCH 15/21] Fix warnings --- crates/project_panel2/src/project_panel.rs | 246 ++++++++++----------- 1 file changed, 119 insertions(+), 127 deletions(-) diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index b2ffc0d332..1feead1a19 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -9,8 +9,8 @@ use file_associations::FileAssociations; use anyhow::{anyhow, Result}; use gpui::{ actions, div, px, rems, svg, uniform_list, Action, AppContext, AssetSource, AsyncWindowContext, - ClipboardItem, Component, Div, Entity, EventEmitter, FocusHandle, FocusableKeyDispatch, Model, - ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, + ClipboardItem, Component, Div, EventEmitter, FocusHandle, FocusableKeyDispatch, Model, + MouseButton, ParentElement as _, Pixels, Point, PromptLevel, Render, StatefulInteractive, StatefulInteractivity, StatelessInteractive, Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext as _, WeakView, WindowContext, }; @@ -54,8 +54,8 @@ pub struct ProjectPanel { edit_state: Option, filename_editor: View, clipboard_entry: Option, - dragged_entry_destination: Option>, - workspace: WeakView, + _dragged_entry_destination: Option>, + _workspace: WeakView, has_focus: bool, width: Option, pending_serialization: Task>, @@ -219,7 +219,6 @@ impl ProjectPanel { // }) // .detach(); - let view_id = cx.view().entity_id(); let mut this = Self { project: project.clone(), fs: workspace.app_state().fs.clone(), @@ -233,8 +232,8 @@ impl ProjectPanel { filename_editor, clipboard_entry: None, // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - dragged_entry_destination: None, - workspace: workspace.weak_handle(), + _dragged_entry_destination: None, + _workspace: workspace.weak_handle(), has_focus: false, width: None, pending_serialization: Task::ready(None), @@ -286,19 +285,19 @@ 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(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); + } + } } _ => {} } @@ -366,79 +365,80 @@ impl ProjectPanel { fn deploy_context_menu( &mut self, - position: Point, - entry_id: ProjectEntryId, - cx: &mut ViewContext, + _position: Point, + _entry_id: ProjectEntryId, + _cx: &mut ViewContext, ) { - // let project = self.project.read(cx); + todo!() + // let project = self.project.read(cx); - // let worktree_id = if let Some(id) = project.worktree_id_for_entry(entry_id, cx) { - // id - // } else { - // return; - // }; + // 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, - // }); + // self.selection = Some(Selection { + // worktree_id, + // 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 mut menu_entries = Vec::new(); + // if let Some((worktree, entry)) = self.selected_entry(cx) { + // let is_root = Some(entry) == worktree.root_entry(); + // if !project.is_remote() { + // menu_entries.push(ContextMenuItem::action( + // "Add Folder to Project", + // workspace::AddFolderToProject, + // )); + // if is_root { + // let project = self.project.clone(); + // menu_entries.push(ContextMenuItem::handler("Remove from Project", move |cx| { + // project.update(cx, |project, cx| project.remove_worktree(worktree_id, cx)); + // })); + // } // } - // } - // menu_entries.push(ContextMenuItem::action("New File", NewFile)); - // menu_entries.push(ContextMenuItem::action("New Folder", NewDirectory)); - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Cut", Cut)); - // menu_entries.push(ContextMenuItem::action("Copy", Copy)); - // if let Some(clipboard_entry) = self.clipboard_entry { - // if clipboard_entry.worktree_id() == worktree.id() { - // menu_entries.push(ContextMenuItem::action("Paste", Paste)); - // } - // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); - // menu_entries.push(ContextMenuItem::action( - // "Copy Relative Path", - // CopyRelativePath, - // )); - - // if entry.is_dir() { + // 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("Reveal in Finder", RevealInFinder)); - // if entry.is_dir() { - // menu_entries.push(ContextMenuItem::action("Open in Terminal", OpenInTerminal)); + // menu_entries.push(ContextMenuItem::action("Cut", Cut)); + // menu_entries.push(ContextMenuItem::action("Copy", Copy)); + // if let Some(clipboard_entry) = self.clipboard_entry { + // if clipboard_entry.worktree_id() == worktree.id() { + // menu_entries.push(ContextMenuItem::action("Paste", Paste)); + // } + // } + // menu_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Copy Path", CopyPath)); // menu_entries.push(ContextMenuItem::action( - // "Search Inside", - // NewSearchInDirectory, + // "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_entries.push(ContextMenuItem::Separator); + // menu_entries.push(ContextMenuItem::action("Rename", Rename)); + // if !is_root { + // menu_entries.push(ContextMenuItem::action("Delete", Delete)); + // } // } - // menu_entries.push(ContextMenuItem::Separator); - // menu_entries.push(ContextMenuItem::action("Rename", Rename)); - // if !is_root { - // menu_entries.push(ContextMenuItem::action("Delete", Delete)); - // } - // } + // // self.context_menu.update(cx, |menu, cx| { + // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); + // // }); - // // self.context_menu.update(cx, |menu, cx| { - // // menu.show(position, AnchorCorner::TopLeft, menu_entries, cx); - // // }); - - // cx.notify(); + // cx.notify(); } fn expand_selected_entry(&mut self, _: &ExpandSelectedEntry, cx: &mut ViewContext) { @@ -955,7 +955,7 @@ impl ProjectPanel { } } - fn open_in_terminal(&mut self, _: &OpenInTerminal, cx: &mut ViewContext) { + fn open_in_terminal(&mut self, _: &OpenInTerminal, _cx: &mut ViewContext) { todo!() // if let Some((worktree, entry)) = self.selected_entry(cx) { // let window = cx.window(); @@ -990,36 +990,37 @@ impl ProjectPanel { } } - fn move_entry( - &mut self, - entry_to_move: ProjectEntryId, - destination: ProjectEntryId, - destination_is_file: bool, - cx: &mut ViewContext, - ) { - let destination_worktree = self.project.update(cx, |project, cx| { - let entry_path = project.path_for_entry(entry_to_move, cx)?; - let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); + // todo!() + // fn move_entry( + // &mut self, + // entry_to_move: ProjectEntryId, + // destination: ProjectEntryId, + // destination_is_file: bool, + // cx: &mut ViewContext, + // ) { + // let destination_worktree = self.project.update(cx, |project, cx| { + // let entry_path = project.path_for_entry(entry_to_move, cx)?; + // let destination_entry_path = project.path_for_entry(destination, cx)?.path.clone(); - let mut destination_path = destination_entry_path.as_ref(); - if destination_is_file { - destination_path = destination_path.parent()?; - } + // let mut 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); - } + // 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)?) - }); + // Some(project.worktree_id_for_entry(destination, cx)?) + // }); - if let Some(destination_worktree) = destination_worktree { - self.expand_entry(destination_worktree, 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; @@ -1367,7 +1368,9 @@ impl ProjectPanel { if let (Some(editor), true) = (editor, show_editor) { div().w_full().child(editor.clone()) } else { - div().child(Label::new(details.filename.clone())) + div() + .text_color(filename_text_color) + .child(Label::new(details.filename.clone())) } .ml_1(), ) @@ -1411,21 +1414,16 @@ impl ProjectPanel { } } }) - // .on_down(MouseButton::Right, move |event, this, cx| { - // this.deploy_context_menu(event.position, entry_id, cx); - // }) - // .on_up(MouseButton::Left, move |_, this, cx| { - // if let Some((_, dragged_entry)) = cx - // .global::>() - // .currently_dragged::(cx.window()) - // { + .on_mouse_down(MouseButton::Right, move |this, event, cx| { + this.deploy_context_menu(event.position, entry_id, cx); + }) + // .on_drop::(|this, event, cx| { // this.move_entry( // *dragged_entry, // entry_id, // matches!(details.kind, EntryKind::File(_)), // cx, // ); - // } // }) } } @@ -1433,10 +1431,7 @@ impl ProjectPanel { impl Render for ProjectPanel { type Element = Div, FocusableKeyDispatch>; - fn render(&mut self, cx: &mut gpui::ViewContext) -> Self::Element { - let theme = cx.theme(); - let last_worktree_root_id = self.last_worktree_root_id; - + fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { let has_worktree = self.visible_entries.len() != 0; if has_worktree { @@ -1475,10 +1470,7 @@ impl Render for ProjectPanel { |this: &mut Self, range, cx| { let mut items = SmallVec::new(); this.for_each_visible_entry(range, cx, |id, details, cx| { - items.push(this.render_entry( - id, details, // &mut dragged_entry_destination, - cx, - )); + items.push(this.render_entry(id, details, cx)); }); items }, From 251b4640c6e4a03d5cc30d6f2e6a08b7e135b45e Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Tue, 14 Nov 2023 12:59:53 -0500 Subject: [PATCH 16/21] Extend tooltip to take meta + kb --- crates/ui2/src/components/tooltip.rs | 35 ++++++++++++++++++++---- crates/workspace2/src/workspace2.rs | 41 ++++++++++++++++++++++------ 2 files changed, 63 insertions(+), 13 deletions(-) diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index ee3e9708c0..0d4f10e35e 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,32 +1,57 @@ -use gpui::{div, Div, ParentElement, Render, SharedString, Styled, ViewContext}; +use gpui::{div, Component, Div, ParentElement, Render, SharedString, Styled, ViewContext}; use theme2::ActiveTheme; -use crate::StyledExt; +use crate::{h_stack, v_stack, Label, LabelColor, StyledExt}; + +use super::keybinding; #[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, + meta: Option, + keybinding: Option, } impl TextTooltip { pub fn new(title: impl Into) -> Self { Self { title: title.into(), + meta: None, + keybinding: None, } } + + pub fn meta(mut self, meta: impl Into) -> Self { + self.meta = Some(meta.into()); + self + } + + pub fn keybinding(mut self, keybinding: impl Into) -> Self { + self.keybinding = Some(keybinding.into()); + self + } } impl Render for TextTooltip { type Element = Div; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { - div() + v_stack() .elevation_2(cx) .font("Zed Sans") - .text_ui() + .text_ui_sm() .text_color(cx.theme().colors().text) .py_1() .px_2() - .child(self.title.clone()) + .child(h_stack().child(self.title.clone()).when_some( + self.keybinding.clone(), + |this, keybinding| { + this.justify_between() + .child(Label::new(keybinding).color(LabelColor::Muted)) + }, + )) + .when_some(self.meta.clone(), |this, meta| { + this.child(Label::new(meta).color(LabelColor::Muted)) + }) } } diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 575ab6b8bd..eb7ef7608e 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, Label, LabelColor}; +use ui::{h_stack, Button, ButtonVariant, Label, LabelColor, TextTooltip}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2660,17 +2660,42 @@ impl Workspace { h_stack() // TODO - Add player menu .child( - Button::new("player") - .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Player(0))), + div() + .id("project_owner_indicator") + .child( + Button::new("player") + .variant(ButtonVariant::Ghost) + .color(Some(LabelColor::Player(0))), + ) + .tooltip(move |_, cx| { + cx.build_view(|cx| TextTooltip::new("Toggle following")) + }), ) // TODO - Add project menu - .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .child( + div() + .id("titlebar_project_menu_button") + .child(Button::new("project_name").variant(ButtonVariant::Ghost)) + .tooltip(move |_, cx| { + cx.build_view(|cx| TextTooltip::new("Recent Projects")) + }), + ) // TODO - Add git menu .child( - Button::new("branch_name") - .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Muted)), + div() + .id("titlebar_git_menu_button") + .child( + Button::new("branch_name") + .variant(ButtonVariant::Ghost) + .color(Some(LabelColor::Muted)), + ) + .tooltip(move |_, cx| { + cx.build_view(|cx| { + TextTooltip::new("Recent Branches") + .keybinding("⌘B") + .meta("Only local branches shown") + }) + }), ), ) // self.titlebar_item .child(h_stack().child(Label::new("Right side titlebar item"))) From 27574524b857881227962daae95be05bcfb8b056 Mon Sep 17 00:00:00 2001 From: Mikayla Date: Tue, 14 Nov 2023 10:31:55 -0800 Subject: [PATCH 17/21] Restore quit action --- crates/gpui2/src/window.rs | 6 ++++ crates/zed2/src/zed2.rs | 63 ++++++++++++++++++++++---------------- 2 files changed, 42 insertions(+), 27 deletions(-) diff --git a/crates/gpui2/src/window.rs b/crates/gpui2/src/window.rs index 4ad807b357..efb586fe03 100644 --- a/crates/gpui2/src/window.rs +++ b/crates/gpui2/src/window.rs @@ -2368,6 +2368,12 @@ impl WindowHandle { { cx.read_window(self, |root_view, _cx| root_view.clone()) } + + pub fn is_active(&self, cx: &WindowContext) -> Option { + cx.windows + .get(self.id) + .and_then(|window| window.as_ref().map(|window| window.active)) + } } impl Copy for WindowHandle {} diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 8f4a9c6ddf..73faeaaaf4 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -460,36 +460,45 @@ fn quit(_: &mut Workspace, _: &Quit, cx: &mut gpui::ViewContext) { .collect::>() })?; - // // If multiple windows have unsaved changes, and need a save prompt, - // // prompt in the active window before switching to a different window. - // workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + // If multiple windows have unsaved changes, and need a save prompt, + // prompt in the active window before switching to a different window. + cx.update(|_, cx| { + workspace_windows.sort_by_key(|window| window.is_active(&cx) == Some(false)); + }) + .log_err(); - // if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { - // let answer = window.prompt( - // PromptLevel::Info, - // "Are you sure you want to quit?", - // &["Quit", "Cancel"], - // &mut cx, - // ); + if let (true, Some(window)) = (should_confirm, workspace_windows.first().copied()) { + let answer = cx + .update(|_, cx| { + cx.prompt( + PromptLevel::Info, + "Are you sure you want to quit?", + &["Quit", "Cancel"], + ) + }) + .log_err(); - // if let Some(mut answer) = answer { - // let answer = answer.next().await; - // if answer != Some(0) { - // return Ok(()); - // } - // } - // } + if let Some(mut answer) = answer { + let answer = answer.await.ok(); + if answer != Some(0) { + return Ok(()); + } + } + } - // // If the user cancels any save prompt, then keep the app open. - // for window in workspace_windows { - // if let Some(should_close) = window.update_root(&mut cx, |workspace, cx| { - // workspace.prepare_to_close(true, cx) - // }) { - // if !should_close.await? { - // return Ok(()); - // } - // } - // } + // If the user cancels any save prompt, then keep the app open. + for window in workspace_windows { + if let Some(should_close) = window + .update(&mut cx, |workspace, cx| { + workspace.prepare_to_close(true, cx) + }) + .log_err() + { + if !should_close.await? { + return Ok(()); + } + } + } cx.update(|_, cx| { cx.quit(); })?; From 90d7033fd046cfb29659da420ebc022679f2e408 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:36:03 -0500 Subject: [PATCH 18/21] Pass `KeyBinding`s to `TextTooltip`s --- crates/ui2/src/components/keybinding.rs | 2 +- crates/ui2/src/components/tooltip.rs | 30 ++++++++++++------------- crates/workspace2/src/workspace2.rs | 12 ++++++++-- 3 files changed, 25 insertions(+), 19 deletions(-) diff --git a/crates/ui2/src/components/keybinding.rs b/crates/ui2/src/components/keybinding.rs index a3e5a870a6..04e036f365 100644 --- a/crates/ui2/src/components/keybinding.rs +++ b/crates/ui2/src/components/keybinding.rs @@ -3,7 +3,7 @@ use strum::EnumIter; use crate::prelude::*; -#[derive(Component)] +#[derive(Component, Clone)] pub struct KeyBinding { /// A keybinding consists of a key and a set of modifier keys. /// More then one keybinding produces a chord. diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 0d4f10e35e..8f31d77b67 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,15 +1,13 @@ -use gpui::{div, Component, Div, ParentElement, Render, SharedString, Styled, ViewContext}; +use gpui::{Div, Render}; use theme2::ActiveTheme; -use crate::{h_stack, v_stack, Label, LabelColor, StyledExt}; +use crate::prelude::*; +use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor, StyledExt}; -use super::keybinding; - -#[derive(Clone, Debug)] pub struct TextTooltip { title: SharedString, meta: Option, - keybinding: Option, + key_binding: Option, } impl TextTooltip { @@ -17,7 +15,7 @@ impl TextTooltip { Self { title: title.into(), meta: None, - keybinding: None, + key_binding: None, } } @@ -26,8 +24,8 @@ impl TextTooltip { self } - pub fn keybinding(mut self, keybinding: impl Into) -> Self { - self.keybinding = Some(keybinding.into()); + pub fn key_binding(mut self, key_binding: impl Into>) -> Self { + self.key_binding = key_binding.into(); self } } @@ -43,13 +41,13 @@ impl Render for TextTooltip { .text_color(cx.theme().colors().text) .py_1() .px_2() - .child(h_stack().child(self.title.clone()).when_some( - self.keybinding.clone(), - |this, keybinding| { - this.justify_between() - .child(Label::new(keybinding).color(LabelColor::Muted)) - }, - )) + .child( + h_stack() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) + }), + ) .when_some(self.meta.clone(), |this, meta| { this.child(Label::new(meta).color(LabelColor::Muted)) }) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index a036b030c9..88e8dc7934 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, Label, LabelColor, TextTooltip}; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, LabelColor, TextTooltip}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2502,9 +2502,17 @@ impl Workspace { .color(Some(LabelColor::Muted)), ) .tooltip(move |_, cx| { + // todo!() Replace with real action. + #[gpui::action] + struct NoAction {} + cx.build_view(|cx| { TextTooltip::new("Recent Branches") - .keybinding("⌘B") + .key_binding(KeyBinding::new(gpui::KeyBinding::new( + "cmd-b", + NoAction {}, + None, + ))) .meta("Only local branches shown") }) }), From 9d31523cf3056df980d47ff44f3439a0ce15d51e Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:37:21 -0500 Subject: [PATCH 19/21] Rename `keybinding` method on `PaletteItem` to `key_binding` --- crates/ui2/src/components/palette.rs | 25 +++++++++++-------------- crates/ui2/src/static_data.rs | 20 ++++++++++---------- 2 files changed, 21 insertions(+), 24 deletions(-) diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index 4e1034595d..d73b15940e 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -108,7 +108,7 @@ impl Palette { pub struct PaletteItem { pub label: SharedString, pub sublabel: Option, - pub keybinding: Option, + pub key_binding: Option, } impl PaletteItem { @@ -116,7 +116,7 @@ impl PaletteItem { Self { label: label.into(), sublabel: None, - keybinding: None, + key_binding: None, } } @@ -130,11 +130,8 @@ impl PaletteItem { self } - pub fn keybinding(mut self, keybinding: K) -> Self - where - K: Into>, - { - self.keybinding = keybinding.into(); + pub fn key_binding(mut self, key_binding: impl Into>) -> Self { + self.key_binding = key_binding.into(); self } @@ -149,7 +146,7 @@ impl PaletteItem { .child(Label::new(self.label.clone())) .children(self.sublabel.clone().map(|sublabel| Label::new(sublabel))), ) - .children(self.keybinding) + .children(self.key_binding) } } @@ -182,23 +179,23 @@ mod stories { .placeholder("Execute a command...") .items(vec![ PaletteItem::new("theme selector: toggle") - .keybinding(KeyBinding::new(binding("cmd-k cmd-t"))), + .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))), PaletteItem::new("assistant: inline assist") - .keybinding(KeyBinding::new(binding("cmd-enter"))), + .key_binding(KeyBinding::new(binding("cmd-enter"))), PaletteItem::new("assistant: quote selection") - .keybinding(KeyBinding::new(binding("cmd-<"))), + .key_binding(KeyBinding::new(binding("cmd-<"))), PaletteItem::new("assistant: toggle focus") - .keybinding(KeyBinding::new(binding("cmd-?"))), + .key_binding(KeyBinding::new(binding("cmd-?"))), PaletteItem::new("auto update: check"), PaletteItem::new("auto update: view release notes"), PaletteItem::new("branches: open recent") - .keybinding(KeyBinding::new(binding("cmd-alt-b"))), + .key_binding(KeyBinding::new(binding("cmd-alt-b"))), PaletteItem::new("chat panel: toggle focus"), PaletteItem::new("cli: install"), PaletteItem::new("client: sign in"), PaletteItem::new("client: sign out"), PaletteItem::new("editor: cancel") - .keybinding(KeyBinding::new(binding("escape"))), + .key_binding(KeyBinding::new(binding("escape"))), ]), ) } diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 89aef8140a..4615adbfa4 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -701,16 +701,16 @@ pub fn static_collab_panel_channels() -> Vec { pub fn example_editor_actions() -> Vec { vec![ - PaletteItem::new("New File").keybinding(KeyBinding::new(binding("cmd-n"))), - PaletteItem::new("Open File").keybinding(KeyBinding::new(binding("cmd-o"))), - PaletteItem::new("Save File").keybinding(KeyBinding::new(binding("cmd-s"))), - PaletteItem::new("Cut").keybinding(KeyBinding::new(binding("cmd-x"))), - PaletteItem::new("Copy").keybinding(KeyBinding::new(binding("cmd-c"))), - PaletteItem::new("Paste").keybinding(KeyBinding::new(binding("cmd-v"))), - PaletteItem::new("Undo").keybinding(KeyBinding::new(binding("cmd-z"))), - PaletteItem::new("Redo").keybinding(KeyBinding::new(binding("cmd-shift-z"))), - PaletteItem::new("Find").keybinding(KeyBinding::new(binding("cmd-f"))), - PaletteItem::new("Replace").keybinding(KeyBinding::new(binding("cmd-r"))), + PaletteItem::new("New File").key_binding(KeyBinding::new(binding("cmd-n"))), + PaletteItem::new("Open File").key_binding(KeyBinding::new(binding("cmd-o"))), + PaletteItem::new("Save File").key_binding(KeyBinding::new(binding("cmd-s"))), + PaletteItem::new("Cut").key_binding(KeyBinding::new(binding("cmd-x"))), + PaletteItem::new("Copy").key_binding(KeyBinding::new(binding("cmd-c"))), + PaletteItem::new("Paste").key_binding(KeyBinding::new(binding("cmd-v"))), + PaletteItem::new("Undo").key_binding(KeyBinding::new(binding("cmd-z"))), + PaletteItem::new("Redo").key_binding(KeyBinding::new(binding("cmd-shift-z"))), + PaletteItem::new("Find").key_binding(KeyBinding::new(binding("cmd-f"))), + PaletteItem::new("Replace").key_binding(KeyBinding::new(binding("cmd-r"))), PaletteItem::new("Jump to Line"), PaletteItem::new("Select All"), PaletteItem::new("Deselect All"), From dc56a7b12bf511eafb0f472d9c6f359e9fc41a19 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:43:37 -0500 Subject: [PATCH 20/21] Add `LabelSize` --- crates/ui2/src/components/label.rs | 30 +++++++++++++++++++++++++++- crates/ui2/src/components/tooltip.rs | 8 ++++++-- 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index 4b9cea8dc2..c316a07483 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -3,6 +3,13 @@ use gpui::{relative, Hsla, Text, TextRun, WindowContext}; use crate::prelude::*; use crate::styled_ext::StyledExt; +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)] +pub enum LabelSize { + #[default] + Default, + Small, +} + #[derive(Default, PartialEq, Copy, Clone)] pub enum LabelColor { #[default] @@ -56,6 +63,7 @@ pub enum LineHeightStyle { #[derive(Component)] pub struct Label { label: SharedString, + size: LabelSize, line_height_style: LineHeightStyle, color: LabelColor, strikethrough: bool, @@ -65,12 +73,18 @@ impl Label { pub fn new(label: impl Into) -> Self { Self { label: label.into(), + size: LabelSize::Default, line_height_style: LineHeightStyle::default(), color: LabelColor::Default, strikethrough: false, } } + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + pub fn color(mut self, color: LabelColor) -> Self { self.color = color; self @@ -98,7 +112,10 @@ impl Label { .bg(LabelColor::Hidden.hsla(cx)), ) }) - .text_ui() + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) .when(self.line_height_style == LineHeightStyle::UILabel, |this| { this.line_height(relative(1.)) }) @@ -110,6 +127,7 @@ impl Label { #[derive(Component)] pub struct HighlightedLabel { label: SharedString, + size: LabelSize, color: LabelColor, highlight_indices: Vec, strikethrough: bool, @@ -121,12 +139,18 @@ impl HighlightedLabel { pub fn new(label: impl Into, highlight_indices: Vec) -> Self { Self { label: label.into(), + size: LabelSize::Default, color: LabelColor::Default, highlight_indices, strikethrough: false, } } + pub fn size(mut self, size: LabelSize) -> Self { + self.size = size; + self + } + pub fn color(mut self, color: LabelColor) -> Self { self.color = color; self @@ -186,6 +210,10 @@ impl HighlightedLabel { .bg(LabelColor::Hidden.hsla(cx)), ) }) + .map(|this| match self.size { + LabelSize::Default => this.text_ui(), + LabelSize::Small => this.text_ui_sm(), + }) .child(Text::styled(self.label, runs)) } } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 8f31d77b67..536bb22ba0 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,8 +1,8 @@ use gpui::{Div, Render}; use theme2::ActiveTheme; -use crate::prelude::*; use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor, StyledExt}; +use crate::{prelude::*, LabelSize}; pub struct TextTooltip { title: SharedString, @@ -49,7 +49,11 @@ impl Render for TextTooltip { }), ) .when_some(self.meta.clone(), |this, meta| { - this.child(Label::new(meta).color(LabelColor::Muted)) + this.child( + Label::new(meta) + .size(LabelSize::Small) + .color(LabelColor::Muted), + ) }) } } From 76c15229c18f9a008209a280212d55ff5fe85930 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Tue, 14 Nov 2023 13:48:01 -0500 Subject: [PATCH 21/21] Combine `LabelColor` and `IconColor` into `TextColor` --- crates/editor2/src/items.rs | 4 +- crates/go_to_line2/src/go_to_line.rs | 4 +- crates/picker2/src/picker2.rs | 4 +- crates/ui2/src/components/button.rs | 44 ++++++----- crates/ui2/src/components/checkbox.rs | 10 +-- crates/ui2/src/components/icon.rs | 74 ++----------------- crates/ui2/src/components/icon_button.rs | 10 +-- crates/ui2/src/components/input.rs | 10 +-- crates/ui2/src/components/label.rs | 56 +++++++------- crates/ui2/src/components/list.rs | 20 ++--- crates/ui2/src/components/palette.rs | 6 +- crates/ui2/src/components/tab.rs | 16 ++-- crates/ui2/src/components/toggle.rs | 6 +- crates/ui2/src/components/tooltip.rs | 6 +- crates/ui2/src/prelude.rs | 2 +- crates/ui2/src/static_data.rs | 36 ++++----- crates/ui2/src/to_extract/buffer_search.rs | 4 +- crates/ui2/src/to_extract/chat_panel.rs | 4 +- crates/ui2/src/to_extract/copilot.rs | 4 +- crates/ui2/src/to_extract/editor_pane.rs | 4 +- .../ui2/src/to_extract/notifications_panel.rs | 14 ++-- crates/ui2/src/to_extract/status_bar.rs | 12 +-- crates/ui2/src/to_extract/title_bar.rs | 10 +-- crates/workspace2/src/pane.rs | 6 +- crates/workspace2/src/workspace2.rs | 6 +- 25 files changed, 152 insertions(+), 220 deletions(-) diff --git a/crates/editor2/src/items.rs b/crates/editor2/src/items.rs index 9614082ccf..3a9e6c2a65 100644 --- a/crates/editor2/src/items.rs +++ b/crates/editor2/src/items.rs @@ -30,7 +30,7 @@ use std::{ }; use text::Selection; use theme::{ActiveTheme, Theme}; -use ui::{Label, LabelColor}; +use ui::{Label, TextColor}; use util::{paths::PathExt, ResultExt, TryFutureExt}; use workspace::item::{BreadcrumbText, FollowEvent, FollowableEvents, FollowableItemHandle}; use workspace::{ @@ -607,7 +607,7 @@ impl Item for Editor { &description, MAX_TAB_TITLE_LEN, )) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) })), diff --git a/crates/go_to_line2/src/go_to_line.rs b/crates/go_to_line2/src/go_to_line.rs index 1d57be6fd0..a16ff85ff2 100644 --- a/crates/go_to_line2/src/go_to_line.rs +++ b/crates/go_to_line2/src/go_to_line.rs @@ -5,7 +5,7 @@ use gpui::{ }; use text::{Bias, Point}; use theme::ActiveTheme; -use ui::{h_stack, v_stack, Label, LabelColor, StyledExt}; +use ui::{h_stack, v_stack, Label, StyledExt, TextColor}; use util::paths::FILE_ROW_COLUMN_DELIMITER; use workspace::{Modal, ModalEvent, Workspace}; @@ -176,7 +176,7 @@ impl Render for GoToLine { .justify_between() .px_2() .py_1() - .child(Label::new(self.current_text.clone()).color(LabelColor::Muted)), + .child(Label::new(self.current_text.clone()).color(TextColor::Muted)), ), ) } diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 0cfe5c8992..70cef67868 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -4,7 +4,7 @@ use gpui::{ Styled, Task, UniformListScrollHandle, View, ViewContext, VisualContext, WindowContext, }; use std::{cmp, sync::Arc}; -use ui::{prelude::*, v_stack, Divider, Label, LabelColor}; +use ui::{prelude::*, v_stack, Divider, Label, TextColor}; pub struct Picker { pub delegate: D, @@ -224,7 +224,7 @@ impl Render for Picker { v_stack().p_1().grow().child( div() .px_1() - .child(Label::new("No matches").color(LabelColor::Muted)), + .child(Label::new("No matches").color(TextColor::Muted)), ), ) }) diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 1418a977f1..f3f3ba6a50 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -2,10 +2,8 @@ use std::sync::Arc; use gpui::{div, DefiniteLength, Hsla, MouseButton, WindowContext}; -use crate::{ - h_stack, prelude::*, Icon, IconButton, IconColor, IconElement, Label, LabelColor, - LineHeightStyle, -}; +use crate::prelude::*; +use crate::{h_stack, Icon, IconButton, IconElement, Label, LineHeightStyle, TextColor}; /// Provides the flexibility to use either a standard /// button or an icon button in a given context. @@ -87,7 +85,7 @@ pub struct Button { label: SharedString, variant: ButtonVariant, width: Option, - color: Option, + color: Option, } impl Button { @@ -141,14 +139,14 @@ impl Button { self } - pub fn color(mut self, color: Option) -> Self { + pub fn color(mut self, color: Option) -> Self { self.color = color; self } - pub fn label_color(&self, color: Option) -> LabelColor { + pub fn label_color(&self, color: Option) -> TextColor { if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else if let Some(color) = color { color } else { @@ -156,21 +154,21 @@ impl Button { } } - fn render_label(&self, color: LabelColor) -> Label { + fn render_label(&self, color: TextColor) -> Label { Label::new(self.label.clone()) .color(color) .line_height_style(LineHeightStyle::UILabel) } - fn render_icon(&self, icon_color: IconColor) -> Option { + fn render_icon(&self, icon_color: TextColor) -> Option { self.icon.map(|i| IconElement::new(i).color(icon_color)) } pub fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let (icon_color, label_color) = match (self.disabled, self.color) { - (true, _) => (IconColor::Disabled, LabelColor::Disabled), - (_, None) => (IconColor::Default, LabelColor::Default), - (_, Some(color)) => (IconColor::from(color), color), + (true, _) => (TextColor::Disabled, TextColor::Disabled), + (_, None) => (TextColor::Default, TextColor::Default), + (_, Some(color)) => (TextColor::from(color), color), }; let mut button = h_stack() @@ -240,7 +238,7 @@ pub use stories::*; #[cfg(feature = "stories")] mod stories { use super::*; - use crate::{h_stack, v_stack, LabelColor, Story}; + use crate::{h_stack, v_stack, Story, TextColor}; use gpui::{rems, Div, Render}; use strum::IntoEnumIterator; @@ -265,7 +263,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label").variant(ButtonVariant::Ghost), // .state(state), @@ -276,7 +274,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -290,7 +288,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -307,7 +305,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label").variant(ButtonVariant::Filled), // .state(state), @@ -318,7 +316,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -332,7 +330,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -349,7 +347,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -363,7 +361,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") @@ -379,7 +377,7 @@ mod stories { v_stack() .gap_1() .child( - Label::new(state.to_string()).color(LabelColor::Muted), + Label::new(state.to_string()).color(TextColor::Muted), ) .child( Button::new("Label") diff --git a/crates/ui2/src/components/checkbox.rs b/crates/ui2/src/components/checkbox.rs index 20dad74712..3480c8cb72 100644 --- a/crates/ui2/src/components/checkbox.rs +++ b/crates/ui2/src/components/checkbox.rs @@ -6,7 +6,7 @@ use gpui::{ }; use theme2::ActiveTheme; -use crate::{Icon, IconColor, IconElement, Selection}; +use crate::{Icon, IconElement, Selection, TextColor}; pub type CheckHandler = Arc) + Send + Sync>; @@ -58,9 +58,9 @@ impl Checkbox { .color( // If the checkbox is disabled we change the color of the icon. if self.disabled { - IconColor::Disabled + TextColor::Disabled } else { - IconColor::Selected + TextColor::Selected }, ), ) @@ -73,9 +73,9 @@ impl Checkbox { .color( // If the checkbox is disabled we change the color of the icon. if self.disabled { - IconColor::Disabled + TextColor::Disabled } else { - IconColor::Selected + TextColor::Selected }, ), ) diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index 75c8129608..5b60421205 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -1,7 +1,7 @@ -use gpui::{rems, svg, Hsla}; +use gpui::{rems, svg}; use strum::EnumIter; -use crate::{prelude::*, LabelColor}; +use crate::prelude::*; #[derive(Default, PartialEq, Copy, Clone)] pub enum IconSize { @@ -10,70 +10,6 @@ pub enum IconSize { Medium, } -#[derive(Default, PartialEq, Copy, Clone)] -pub enum IconColor { - #[default] - Default, - Accent, - Created, - Deleted, - Disabled, - Error, - Hidden, - Info, - Modified, - Muted, - Placeholder, - Player(u32), - Selected, - Success, - Warning, -} - -impl IconColor { - pub fn color(self, cx: &WindowContext) -> Hsla { - match self { - IconColor::Default => cx.theme().colors().icon, - IconColor::Muted => cx.theme().colors().icon_muted, - IconColor::Disabled => cx.theme().colors().icon_disabled, - IconColor::Placeholder => cx.theme().colors().icon_placeholder, - IconColor::Accent => cx.theme().colors().icon_accent, - IconColor::Error => cx.theme().status().error, - IconColor::Warning => cx.theme().status().warning, - IconColor::Success => cx.theme().status().success, - IconColor::Info => cx.theme().status().info, - IconColor::Selected => cx.theme().colors().icon_accent, - IconColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, - IconColor::Created => cx.theme().status().created, - IconColor::Modified => cx.theme().status().modified, - IconColor::Deleted => cx.theme().status().deleted, - IconColor::Hidden => cx.theme().status().hidden, - } - } -} - -impl From for IconColor { - fn from(label: LabelColor) -> Self { - match label { - LabelColor::Default => IconColor::Default, - LabelColor::Muted => IconColor::Muted, - LabelColor::Disabled => IconColor::Disabled, - LabelColor::Placeholder => IconColor::Placeholder, - LabelColor::Accent => IconColor::Accent, - LabelColor::Error => IconColor::Error, - LabelColor::Warning => IconColor::Warning, - LabelColor::Success => IconColor::Success, - LabelColor::Info => IconColor::Info, - LabelColor::Selected => IconColor::Selected, - LabelColor::Player(i) => IconColor::Player(i), - LabelColor::Created => IconColor::Created, - LabelColor::Modified => IconColor::Modified, - LabelColor::Deleted => IconColor::Deleted, - LabelColor::Hidden => IconColor::Hidden, - } - } -} - #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum Icon { Ai, @@ -194,7 +130,7 @@ impl Icon { #[derive(Component)] pub struct IconElement { icon: Icon, - color: IconColor, + color: TextColor, size: IconSize, } @@ -202,12 +138,12 @@ impl IconElement { pub fn new(icon: Icon) -> Self { Self { icon, - color: IconColor::default(), + color: TextColor::default(), size: IconSize::default(), } } - pub fn color(mut self, color: IconColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index b20cd31036..b719a05b92 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,4 +1,4 @@ -use crate::{h_stack, prelude::*, ClickHandler, Icon, IconColor, IconElement, TextTooltip}; +use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextColor, TextTooltip}; use gpui::{MouseButton, VisualContext}; use std::sync::Arc; @@ -16,7 +16,7 @@ impl Default for IconButtonHandlers { pub struct IconButton { id: ElementId, icon: Icon, - color: IconColor, + color: TextColor, variant: ButtonVariant, state: InteractionState, tooltip: Option, @@ -28,7 +28,7 @@ impl IconButton { Self { id: id.into(), icon, - color: IconColor::default(), + color: TextColor::default(), variant: ButtonVariant::default(), state: InteractionState::default(), tooltip: None, @@ -41,7 +41,7 @@ impl IconButton { self } - pub fn color(mut self, color: IconColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -71,7 +71,7 @@ impl IconButton { fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { - (InteractionState::Disabled, _) => IconColor::Disabled, + (InteractionState::Disabled, _) => TextColor::Disabled, _ => self.color, }; diff --git a/crates/ui2/src/components/input.rs b/crates/ui2/src/components/input.rs index 1a44827fe8..9bcf5e4dba 100644 --- a/crates/ui2/src/components/input.rs +++ b/crates/ui2/src/components/input.rs @@ -1,6 +1,6 @@ use crate::prelude::*; use crate::Label; -use crate::LabelColor; +use crate::TextColor; #[derive(Default, PartialEq)] pub enum InputVariant { @@ -71,15 +71,15 @@ impl Input { }; let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else { - LabelColor::Placeholder + TextColor::Placeholder }); let label = Label::new(self.value.clone()).color(if self.disabled { - LabelColor::Disabled + TextColor::Disabled } else { - LabelColor::Default + TextColor::Default }); div() diff --git a/crates/ui2/src/components/label.rs b/crates/ui2/src/components/label.rs index c316a07483..cbb75278c2 100644 --- a/crates/ui2/src/components/label.rs +++ b/crates/ui2/src/components/label.rs @@ -11,7 +11,7 @@ pub enum LabelSize { } #[derive(Default, PartialEq, Copy, Clone)] -pub enum LabelColor { +pub enum TextColor { #[default] Default, Accent, @@ -30,24 +30,24 @@ pub enum LabelColor { Warning, } -impl LabelColor { - pub fn hsla(&self, cx: &WindowContext) -> Hsla { +impl TextColor { + pub fn color(&self, cx: &WindowContext) -> Hsla { match self { - LabelColor::Default => cx.theme().colors().text, - LabelColor::Muted => cx.theme().colors().text_muted, - LabelColor::Created => cx.theme().status().created, - LabelColor::Modified => cx.theme().status().modified, - LabelColor::Deleted => cx.theme().status().deleted, - LabelColor::Disabled => cx.theme().colors().text_disabled, - LabelColor::Hidden => cx.theme().status().hidden, - LabelColor::Info => cx.theme().status().info, - LabelColor::Placeholder => cx.theme().colors().text_placeholder, - LabelColor::Accent => cx.theme().colors().text_accent, - LabelColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, - LabelColor::Error => cx.theme().status().error, - LabelColor::Selected => cx.theme().colors().text_accent, - LabelColor::Success => cx.theme().status().success, - LabelColor::Warning => cx.theme().status().warning, + TextColor::Default => cx.theme().colors().text, + TextColor::Muted => cx.theme().colors().text_muted, + TextColor::Created => cx.theme().status().created, + TextColor::Modified => cx.theme().status().modified, + TextColor::Deleted => cx.theme().status().deleted, + TextColor::Disabled => cx.theme().colors().text_disabled, + TextColor::Hidden => cx.theme().status().hidden, + TextColor::Info => cx.theme().status().info, + TextColor::Placeholder => cx.theme().colors().text_placeholder, + TextColor::Accent => cx.theme().colors().text_accent, + TextColor::Player(i) => cx.theme().styles.player.0[i.clone() as usize].cursor, + TextColor::Error => cx.theme().status().error, + TextColor::Selected => cx.theme().colors().text_accent, + TextColor::Success => cx.theme().status().success, + TextColor::Warning => cx.theme().status().warning, } } } @@ -65,7 +65,7 @@ pub struct Label { label: SharedString, size: LabelSize, line_height_style: LineHeightStyle, - color: LabelColor, + color: TextColor, strikethrough: bool, } @@ -75,7 +75,7 @@ impl Label { label: label.into(), size: LabelSize::Default, line_height_style: LineHeightStyle::default(), - color: LabelColor::Default, + color: TextColor::Default, strikethrough: false, } } @@ -85,7 +85,7 @@ impl Label { self } - pub fn color(mut self, color: LabelColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -109,7 +109,7 @@ impl Label { .top_1_2() .w_full() .h_px() - .bg(LabelColor::Hidden.hsla(cx)), + .bg(TextColor::Hidden.color(cx)), ) }) .map(|this| match self.size { @@ -119,7 +119,7 @@ impl Label { .when(self.line_height_style == LineHeightStyle::UILabel, |this| { this.line_height(relative(1.)) }) - .text_color(self.color.hsla(cx)) + .text_color(self.color.color(cx)) .child(self.label.clone()) } } @@ -128,7 +128,7 @@ impl Label { pub struct HighlightedLabel { label: SharedString, size: LabelSize, - color: LabelColor, + color: TextColor, highlight_indices: Vec, strikethrough: bool, } @@ -140,7 +140,7 @@ impl HighlightedLabel { Self { label: label.into(), size: LabelSize::Default, - color: LabelColor::Default, + color: TextColor::Default, highlight_indices, strikethrough: false, } @@ -151,7 +151,7 @@ impl HighlightedLabel { self } - pub fn color(mut self, color: LabelColor) -> Self { + pub fn color(mut self, color: TextColor) -> Self { self.color = color; self } @@ -170,7 +170,7 @@ impl HighlightedLabel { let mut runs: Vec = Vec::new(); for (char_ix, char) in self.label.char_indices() { - let mut color = self.color.hsla(cx); + let mut color = self.color.color(cx); if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { @@ -207,7 +207,7 @@ impl HighlightedLabel { .my_auto() .w_full() .h_px() - .bg(LabelColor::Hidden.hsla(cx)), + .bg(TextColor::Hidden.color(cx)), ) }) .map(|this| match self.size { diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index 5c42975b17..1ddad269dd 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -1,11 +1,11 @@ use gpui::div; +use crate::prelude::*; use crate::settings::user_settings; use crate::{ - disclosure_control, h_stack, v_stack, Avatar, Icon, IconColor, IconElement, IconSize, Label, - LabelColor, Toggle, + disclosure_control, h_stack, v_stack, Avatar, GraphicSlot, Icon, IconElement, IconSize, Label, + TextColor, Toggle, }; -use crate::{prelude::*, GraphicSlot}; #[derive(Clone, Copy, Default, Debug, PartialEq)] pub enum ListItemVariant { @@ -68,7 +68,7 @@ impl ListHeader { .items_center() .children(icons.into_iter().map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })), ), @@ -106,10 +106,10 @@ impl ListHeader { .items_center() .children(self.left_icon.map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child(Label::new(self.label.clone()).color(TextColor::Muted)), ) .child(disclosure_control), ) @@ -157,10 +157,10 @@ impl ListSubHeader { .items_center() .children(self.left_icon.map(|i| { IconElement::new(i) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small) })) - .child(Label::new(self.label.clone()).color(LabelColor::Muted)), + .child(Label::new(self.label.clone()).color(TextColor::Muted)), ), ) } @@ -291,7 +291,7 @@ impl ListEntry { h_stack().child( IconElement::new(i) .size(IconSize::Small) - .color(IconColor::Muted), + .color(TextColor::Muted), ), ), Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), @@ -394,7 +394,7 @@ impl List { (false, _) => div().children(self.items), (true, Toggle::Toggled(false)) => div(), (true, _) => { - div().child(Label::new(self.empty_message.clone()).color(LabelColor::Muted)) + div().child(Label::new(self.empty_message.clone()).color(TextColor::Muted)) } }; diff --git a/crates/ui2/src/components/palette.rs b/crates/ui2/src/components/palette.rs index d73b15940e..f1b50bb56d 100644 --- a/crates/ui2/src/components/palette.rs +++ b/crates/ui2/src/components/palette.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor}; +use crate::{h_stack, v_stack, KeyBinding, Label, TextColor}; #[derive(Component)] pub struct Palette { @@ -54,7 +54,7 @@ impl Palette { v_stack() .gap_px() .child(v_stack().py_0p5().px_1().child(div().px_2().py_0p5().child( - Label::new(self.input_placeholder.clone()).color(LabelColor::Placeholder), + Label::new(self.input_placeholder.clone()).color(TextColor::Placeholder), ))) .child( div() @@ -75,7 +75,7 @@ impl Palette { Some( h_stack().justify_between().px_2().py_1().child( Label::new(self.empty_string.clone()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) } else { diff --git a/crates/ui2/src/components/tab.rs b/crates/ui2/src/components/tab.rs index e936dc924a..7128257628 100644 --- a/crates/ui2/src/components/tab.rs +++ b/crates/ui2/src/components/tab.rs @@ -1,5 +1,5 @@ use crate::prelude::*; -use crate::{Icon, IconColor, IconElement, Label, LabelColor}; +use crate::{Icon, IconElement, Label, TextColor}; use gpui::{red, Div, ElementId, Render, View, VisualContext}; #[derive(Component, Clone)] @@ -92,20 +92,18 @@ impl Tab { let label = match (self.git_status, is_deleted) { (_, true) | (GitStatus::Deleted, false) => Label::new(self.title.clone()) - .color(LabelColor::Hidden) + .color(TextColor::Hidden) .set_strikethrough(true), (GitStatus::None, false) => Label::new(self.title.clone()), - (GitStatus::Created, false) => { - Label::new(self.title.clone()).color(LabelColor::Created) - } + (GitStatus::Created, false) => Label::new(self.title.clone()).color(TextColor::Created), (GitStatus::Modified, false) => { - Label::new(self.title.clone()).color(LabelColor::Modified) + Label::new(self.title.clone()).color(TextColor::Modified) } - (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(LabelColor::Accent), + (GitStatus::Renamed, false) => Label::new(self.title.clone()).color(TextColor::Accent), (GitStatus::Conflict, false) => Label::new(self.title.clone()), }; - let close_icon = || IconElement::new(Icon::Close).color(IconColor::Muted); + let close_icon = || IconElement::new(Icon::Close).color(TextColor::Muted); let (tab_bg, tab_hover_bg, tab_active_bg) = match self.current { false => ( @@ -148,7 +146,7 @@ impl Tab { .children(has_fs_conflict.then(|| { IconElement::new(Icon::ExclamationTriangle) .size(crate::IconSize::Small) - .color(IconColor::Warning) + .color(TextColor::Warning) })) .children(self.icon.map(IconElement::new)) .children(if self.close_side == IconSide::Left { diff --git a/crates/ui2/src/components/toggle.rs b/crates/ui2/src/components/toggle.rs index 368c95662f..1683773e16 100644 --- a/crates/ui2/src/components/toggle.rs +++ b/crates/ui2/src/components/toggle.rs @@ -1,6 +1,6 @@ use gpui::{div, Component, ParentElement}; -use crate::{Icon, IconColor, IconElement, IconSize}; +use crate::{Icon, IconElement, IconSize, TextColor}; /// Whether the entry is toggleable, and if so, whether it is currently toggled. /// @@ -49,12 +49,12 @@ pub fn disclosure_control(toggle: Toggle) -> impl Component { (false, _) => div(), (_, true) => div().child( IconElement::new(Icon::ChevronDown) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small), ), (_, false) => div().child( IconElement::new(Icon::ChevronRight) - .color(IconColor::Muted) + .color(TextColor::Muted) .size(IconSize::Small), ), } diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 536bb22ba0..58375b0b67 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,8 +1,8 @@ use gpui::{Div, Render}; use theme2::ActiveTheme; -use crate::{h_stack, v_stack, KeyBinding, Label, LabelColor, StyledExt}; -use crate::{prelude::*, LabelSize}; +use crate::prelude::*; +use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor}; pub struct TextTooltip { title: SharedString, @@ -52,7 +52,7 @@ impl Render for TextTooltip { this.child( Label::new(meta) .size(LabelSize::Small) - .color(LabelColor::Muted), + .color(TextColor::Muted), ) }) } diff --git a/crates/ui2/src/prelude.rs b/crates/ui2/src/prelude.rs index 545f437a9b..7368118f96 100644 --- a/crates/ui2/src/prelude.rs +++ b/crates/ui2/src/prelude.rs @@ -6,8 +6,8 @@ pub use gpui::{ }; pub use crate::elevation::*; -pub use crate::ButtonVariant; pub use crate::StyledExt; +pub use crate::{ButtonVariant, TextColor}; pub use theme2::ActiveTheme; use gpui::Hsla; diff --git a/crates/ui2/src/static_data.rs b/crates/ui2/src/static_data.rs index 4615adbfa4..bb81d6230f 100644 --- a/crates/ui2/src/static_data.rs +++ b/crates/ui2/src/static_data.rs @@ -10,9 +10,9 @@ use theme2::ActiveTheme; use crate::{binding, HighlightedText}; use crate::{ Buffer, BufferRow, BufferRows, Button, EditorPane, FileSystemStatus, GitStatus, - HighlightedLine, Icon, KeyBinding, Label, LabelColor, ListEntry, ListEntrySize, Livestream, - MicStatus, Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, - PublicPlayer, ScreenShareStatus, Symbol, Tab, Toggle, VideoStatus, + HighlightedLine, Icon, KeyBinding, Label, ListEntry, ListEntrySize, Livestream, MicStatus, + Notification, PaletteItem, Player, PlayerCallStatus, PlayerWithCallStatus, PublicPlayer, + ScreenShareStatus, Symbol, Tab, TextColor, Toggle, VideoStatus, }; use crate::{ListItem, NotificationAction}; @@ -490,20 +490,20 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new(".config")) .left_icon(Icon::Folder.into()) .indent_level(1), - ListEntry::new(Label::new(".git").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".git").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new(".cargo")) .left_icon(Icon::Folder.into()) .indent_level(1), - ListEntry::new(Label::new(".idea").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".idea").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("assets")) .left_icon(Icon::Folder.into()) .indent_level(1) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("cargo-target").color(LabelColor::Hidden)) + ListEntry::new(Label::new("cargo-target").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new("crates")) @@ -528,7 +528,7 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new("call")) .left_icon(Icon::Folder.into()) .indent_level(2), - ListEntry::new(Label::new("sqlez").color(LabelColor::Modified)) + ListEntry::new(Label::new("sqlez").color(TextColor::Modified)) .left_icon(Icon::Folder.into()) .indent_level(2) .toggle(Toggle::Toggled(false)), @@ -543,45 +543,45 @@ pub fn static_project_panel_project_items() -> Vec { ListEntry::new(Label::new("derive_element.rs")) .left_icon(Icon::FileRust.into()) .indent_level(4), - ListEntry::new(Label::new("storybook").color(LabelColor::Modified)) + ListEntry::new(Label::new("storybook").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(1) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("docs").color(LabelColor::Default)) + ListEntry::new(Label::new("docs").color(TextColor::Default)) .left_icon(Icon::Folder.into()) .indent_level(2) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("src").color(LabelColor::Modified)) + ListEntry::new(Label::new("src").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(3) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("ui").color(LabelColor::Modified)) + ListEntry::new(Label::new("ui").color(TextColor::Modified)) .left_icon(Icon::FolderOpen.into()) .indent_level(4) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("component").color(LabelColor::Created)) + ListEntry::new(Label::new("component").color(TextColor::Created)) .left_icon(Icon::FolderOpen.into()) .indent_level(5) .toggle(Toggle::Toggled(true)), - ListEntry::new(Label::new("facepile.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("facepile.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("follow_group.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("follow_group.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("list_item.rs").color(LabelColor::Created)) + ListEntry::new(Label::new("list_item.rs").color(TextColor::Created)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("tab.rs").color(LabelColor::Default)) + ListEntry::new(Label::new("tab.rs").color(TextColor::Default)) .left_icon(Icon::FileRust.into()) .indent_level(6), - ListEntry::new(Label::new("target").color(LabelColor::Hidden)) + ListEntry::new(Label::new("target").color(TextColor::Hidden)) .left_icon(Icon::Folder.into()) .indent_level(1), ListEntry::new(Label::new(".dockerignore")) .left_icon(Icon::FileGeneric.into()) .indent_level(1), - ListEntry::new(Label::new(".DS_Store").color(LabelColor::Hidden)) + ListEntry::new(Label::new(".DS_Store").color(TextColor::Hidden)) .left_icon(Icon::FileGeneric.into()) .indent_level(1), ListEntry::new(Label::new("Cargo.lock")) diff --git a/crates/ui2/src/to_extract/buffer_search.rs b/crates/ui2/src/to_extract/buffer_search.rs index 9993cd3612..996ac6d253 100644 --- a/crates/ui2/src/to_extract/buffer_search.rs +++ b/crates/ui2/src/to_extract/buffer_search.rs @@ -1,7 +1,7 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; -use crate::{h_stack, Icon, IconButton, IconColor, Input}; +use crate::{h_stack, Icon, IconButton, Input, TextColor}; #[derive(Clone)] pub struct BufferSearch { @@ -36,7 +36,7 @@ impl Render for BufferSearch { .child( h_stack().child(Input::new("Search")).child( IconButton::::new("replace", Icon::Replace) - .when(self.is_replace_open, |this| this.color(IconColor::Accent)) + .when(self.is_replace_open, |this| this.color(TextColor::Accent)) .on_click(|buffer_search, cx| { buffer_search.toggle_replace(cx); }), diff --git a/crates/ui2/src/to_extract/chat_panel.rs b/crates/ui2/src/to_extract/chat_panel.rs index 538b5dfceb..b1d208fd67 100644 --- a/crates/ui2/src/to_extract/chat_panel.rs +++ b/crates/ui2/src/to_extract/chat_panel.rs @@ -1,7 +1,7 @@ use chrono::NaiveDateTime; use crate::prelude::*; -use crate::{Icon, IconButton, Input, Label, LabelColor}; +use crate::{Icon, IconButton, Input, Label, TextColor}; #[derive(Component)] pub struct ChatPanel { @@ -95,7 +95,7 @@ impl ChatMessage { .child(Label::new(self.author.clone())) .child( Label::new(self.sent_at.format("%m/%d/%Y").to_string()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), ) .child(div().child(Label::new(self.text.clone()))) diff --git a/crates/ui2/src/to_extract/copilot.rs b/crates/ui2/src/to_extract/copilot.rs index 8750ab3c51..c5622f5be6 100644 --- a/crates/ui2/src/to_extract/copilot.rs +++ b/crates/ui2/src/to_extract/copilot.rs @@ -1,4 +1,4 @@ -use crate::{prelude::*, Button, Label, LabelColor, Modal}; +use crate::{prelude::*, Button, Label, Modal, TextColor}; #[derive(Component)] pub struct CopilotModal { @@ -14,7 +14,7 @@ impl CopilotModal { div().id(self.id.clone()).child( Modal::new("some-id") .title("Connect Copilot to Zed") - .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(LabelColor::Muted)) + .child(Label::new("You can update your settings or sign out from the Copilot menu in the status bar.").color(TextColor::Muted)) .primary_action(Button::new("Connect to Github").variant(ButtonVariant::Filled)), ) } diff --git a/crates/ui2/src/to_extract/editor_pane.rs b/crates/ui2/src/to_extract/editor_pane.rs index fd21e81242..f03323f93f 100644 --- a/crates/ui2/src/to_extract/editor_pane.rs +++ b/crates/ui2/src/to_extract/editor_pane.rs @@ -5,7 +5,7 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::{ hello_world_rust_editor_with_status_example, v_stack, Breadcrumb, Buffer, BufferSearch, Icon, - IconButton, IconColor, Symbol, Tab, TabBar, Toolbar, + IconButton, Symbol, Tab, TabBar, TextColor, Toolbar, }; #[derive(Clone)] @@ -63,7 +63,7 @@ impl Render for EditorPane { IconButton::new("toggle_inlay_hints", Icon::InlayHint), IconButton::::new("buffer_search", Icon::MagnifyingGlass) .when(self.is_buffer_search_open, |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|editor, cx| { editor.toggle_buffer_search(cx); diff --git a/crates/ui2/src/to_extract/notifications_panel.rs b/crates/ui2/src/to_extract/notifications_panel.rs index b2cc4a7846..98e1179851 100644 --- a/crates/ui2/src/to_extract/notifications_panel.rs +++ b/crates/ui2/src/to_extract/notifications_panel.rs @@ -1,8 +1,8 @@ use crate::utils::naive_format_distance_from_now; use crate::{ h_stack, prelude::*, static_new_notification_items_2, v_stack, Avatar, ButtonOrIconButton, - Icon, IconElement, Label, LabelColor, LineHeightStyle, ListHeaderMeta, ListSeparator, - PublicPlayer, UnreadIndicator, + Icon, IconElement, Label, LineHeightStyle, ListHeaderMeta, ListSeparator, PublicPlayer, + TextColor, UnreadIndicator, }; use crate::{ClickHandler, ListHeader}; @@ -48,7 +48,7 @@ impl NotificationsPanel { .border_color(cx.theme().colors().border_variant) .child( Label::new("Search...") - .color(LabelColor::Placeholder) + .color(TextColor::Placeholder) .line_height_style(LineHeightStyle::UILabel), ), ) @@ -252,7 +252,7 @@ impl Notification { if let Some(icon) = icon { meta_el = meta_el.child(IconElement::new(icon.clone())); } - meta_el.child(Label::new(text.clone()).color(LabelColor::Muted)) + meta_el.child(Label::new(text.clone()).color(TextColor::Muted)) }) .collect::>(), ) @@ -311,7 +311,7 @@ impl Notification { true, true, )) - .color(LabelColor::Muted), + .color(TextColor::Muted), ) .child(self.render_meta_items(cx)), ) @@ -321,11 +321,11 @@ impl Notification { // Show the taken_message (Some(_), Some(action_taken)) => h_stack() .children(action_taken.taken_message.0.map(|icon| { - IconElement::new(icon).color(crate::IconColor::Muted) + IconElement::new(icon).color(crate::TextColor::Muted) })) .child( Label::new(action_taken.taken_message.1.clone()) - .color(LabelColor::Muted), + .color(TextColor::Muted), ), // Show the actions (Some(actions), None) => { diff --git a/crates/ui2/src/to_extract/status_bar.rs b/crates/ui2/src/to_extract/status_bar.rs index 34a5993e69..bc236ea1fa 100644 --- a/crates/ui2/src/to_extract/status_bar.rs +++ b/crates/ui2/src/to_extract/status_bar.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::prelude::*; -use crate::{Button, Icon, IconButton, IconColor, ToolDivider, Workspace}; +use crate::{Button, Icon, IconButton, TextColor, ToolDivider, Workspace}; #[derive(Default, PartialEq)] pub enum Tool { @@ -110,7 +110,7 @@ impl StatusBar { .child( IconButton::::new("project_panel", Icon::FileTree) .when(workspace.is_project_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_project_panel(cx); @@ -119,7 +119,7 @@ impl StatusBar { .child( IconButton::::new("collab_panel", Icon::Hash) .when(workspace.is_collab_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_collab_panel(); @@ -174,7 +174,7 @@ impl StatusBar { .child( IconButton::::new("terminal", Icon::Terminal) .when(workspace.is_terminal_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_terminal(cx); @@ -183,7 +183,7 @@ impl StatusBar { .child( IconButton::::new("chat_panel", Icon::MessageBubbles) .when(workspace.is_chat_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_chat_panel(cx); @@ -192,7 +192,7 @@ impl StatusBar { .child( IconButton::::new("assistant_panel", Icon::Ai) .when(workspace.is_assistant_panel_open(), |this| { - this.color(IconColor::Accent) + this.color(TextColor::Accent) }) .on_click(|workspace, cx| { workspace.toggle_assistant_panel(cx); diff --git a/crates/ui2/src/to_extract/title_bar.rs b/crates/ui2/src/to_extract/title_bar.rs index 87d7dd4146..9aa8777a9d 100644 --- a/crates/ui2/src/to_extract/title_bar.rs +++ b/crates/ui2/src/to_extract/title_bar.rs @@ -6,8 +6,8 @@ use gpui::{Div, Render, View, VisualContext}; use crate::prelude::*; use crate::settings::user_settings; use crate::{ - Avatar, Button, Icon, IconButton, IconColor, MicStatus, PlayerStack, PlayerWithCallStatus, - ScreenShareStatus, ToolDivider, TrafficLights, + Avatar, Button, Icon, IconButton, MicStatus, PlayerStack, PlayerWithCallStatus, + ScreenShareStatus, TextColor, ToolDivider, TrafficLights, }; #[derive(Clone)] @@ -152,19 +152,19 @@ impl Render for TitleBar { .gap_1() .child( IconButton::::new("toggle_mic_status", Icon::Mic) - .when(self.is_mic_muted(), |this| this.color(IconColor::Error)) + .when(self.is_mic_muted(), |this| this.color(TextColor::Error)) .on_click(|title_bar, cx| title_bar.toggle_mic_status(cx)), ) .child( IconButton::::new("toggle_deafened", Icon::AudioOn) - .when(self.is_deafened, |this| this.color(IconColor::Error)) + .when(self.is_deafened, |this| this.color(TextColor::Error)) .on_click(|title_bar, cx| title_bar.toggle_deafened(cx)), ) .child( IconButton::::new("toggle_screen_share", Icon::Screen) .when( self.screen_share_status == ScreenShareStatus::Shared, - |this| this.color(IconColor::Accent), + |this| this.color(TextColor::Accent), ) .on_click(|title_bar, cx| { title_bar.toggle_screen_share_status(cx) diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index e3ea4863c9..05dc83673f 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -27,7 +27,7 @@ use std::{ }, }; use ui::v_stack; -use ui::{prelude::*, Icon, IconButton, IconColor, IconElement, TextTooltip}; +use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip}; use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] @@ -1432,13 +1432,13 @@ impl Pane { Some( IconElement::new(Icon::ExclamationTriangle) .size(ui::IconSize::Small) - .color(IconColor::Warning), + .color(TextColor::Warning), ) } else if item.is_dirty(cx) { Some( IconElement::new(Icon::ExclamationTriangle) .size(ui::IconSize::Small) - .color(IconColor::Info), + .color(TextColor::Info), ) } else { None diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 88e8dc7934..4fa09e5b02 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -69,7 +69,7 @@ use std::{ }; use theme2::ActiveTheme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; -use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, LabelColor, TextTooltip}; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip}; use util::ResultExt; use uuid::Uuid; use workspace_settings::{AutosaveSetting, WorkspaceSettings}; @@ -2477,7 +2477,7 @@ impl Workspace { .child( Button::new("player") .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Player(0))), + .color(Some(TextColor::Player(0))), ) .tooltip(move |_, cx| { cx.build_view(|cx| TextTooltip::new("Toggle following")) @@ -2499,7 +2499,7 @@ impl Workspace { .child( Button::new("branch_name") .variant(ButtonVariant::Ghost) - .color(Some(LabelColor::Muted)), + .color(Some(TextColor::Muted)), ) .tooltip(move |_, cx| { // todo!() Replace with real action.