diff --git a/Cargo.lock b/Cargo.lock index 8838fdb13055ea81255cb20b836ce900516f26dc..9c41daa031f4d35967235e8cebc61b5084361150 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9471,6 +9471,27 @@ dependencies = [ "workspace", ] +[[package]] +name = "theme_selector2" +version = "0.1.0" +dependencies = [ + "editor2", + "feature_flags2", + "fs2", + "fuzzy2", + "gpui2", + "log", + "parking_lot 0.11.2", + "picker2", + "postage", + "settings2", + "smol", + "theme2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "thiserror" version = "1.0.48" @@ -11054,6 +11075,31 @@ dependencies = [ "workspace", ] +[[package]] +name = "welcome2" +version = "0.1.0" +dependencies = [ + "anyhow", + "client2", + "db2", + "editor2", + "fs2", + "fuzzy2", + "gpui2", + "install_cli2", + "log", + "picker2", + "project2", + "schemars", + "serde", + "settings2", + "theme2", + "theme_selector2", + "ui2", + "util", + "workspace2", +] + [[package]] name = "which" version = "4.4.2" @@ -11720,6 +11766,7 @@ dependencies = [ "terminal_view2", "text2", "theme2", + "theme_selector2", "thiserror", "tiny_http", "toml 0.5.11", @@ -11757,6 +11804,7 @@ dependencies = [ "urlencoding", "util", "uuid 1.4.1", + "welcome2", "workspace2", "zed_actions2", ] diff --git a/Cargo.toml b/Cargo.toml index 1f6a291a2606e8d41feb18743ae9cc0da142acc5..03a854b77fd1dcae71cb434c2f899eabd19707f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,7 @@ members = [ "crates/theme2", "crates/theme_importer", "crates/theme_selector", + "crates/theme_selector2", "crates/ui2", "crates/util", "crates/semantic_index", @@ -115,6 +116,7 @@ members = [ "crates/vcs_menu", "crates/workspace2", "crates/welcome", + "crates/welcome2", "crates/xtask", "crates/zed", "crates/zed2", diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index b31451aa87a55f5ed65a7a9cdb6a0149d52cfe80..4746c9c6e4222a9c08383001525cf0f9cc55ab09 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -693,8 +693,8 @@ impl Client { } } - pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { - read_credentials_from_keychain(cx).await.is_some() + pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool { + read_credentials_from_keychain(cx).is_some() } #[async_recursion(?Send)] @@ -725,7 +725,7 @@ impl Client { let mut read_from_keychain = false; let mut credentials = self.state.read().credentials.clone(); if credentials.is_none() && try_keychain { - credentials = read_credentials_from_keychain(cx).await; + credentials = read_credentials_from_keychain(cx); read_from_keychain = credentials.is_some(); } if credentials.is_none() { @@ -1324,7 +1324,7 @@ impl Client { } } -async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { +fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option { if IMPERSONATE_LOGIN.is_some() { return None; } diff --git a/crates/command_palette2/src/command_palette.rs b/crates/command_palette2/src/command_palette.rs index 076a2625a19f050c504418990501dd3a0d240814..75e98bfa252e0f37ac358658a595e6d178bdb227 100644 --- a/crates/command_palette2/src/command_palette.rs +++ b/crates/command_palette2/src/command_palette.rs @@ -1,3 +1,8 @@ +use std::{ + cmp::{self, Reverse}, + sync::Arc, +}; + use collections::{CommandPaletteFilter, HashMap}; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{ @@ -5,10 +10,7 @@ use gpui::{ Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView, }; use picker::{Picker, PickerDelegate}; -use std::{ - cmp::{self, Reverse}, - sync::Arc, -}; + use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem}; use util::{ channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL}, diff --git a/crates/gpui2/src/action.rs b/crates/gpui2/src/action.rs index 958eaabdb83076dcd8114ba1f06ae2ac4fc6c50b..03ef2d2281876ca101e210b1d06491413e1ce027 100644 --- a/crates/gpui2/src/action.rs +++ b/crates/gpui2/src/action.rs @@ -162,6 +162,7 @@ macro_rules! actions { ( $name:ident ) => { #[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)] + #[serde(crate = "gpui::serde")] pub struct $name; }; diff --git a/crates/picker2/src/picker2.rs b/crates/picker2/src/picker2.rs index 8ab85a97eccecb83b0621b03a09cbc1409c4df39..672cb1246661a443850e224255098076420fb141 100644 --- a/crates/picker2/src/picker2.rs +++ b/crates/picker2/src/picker2.rs @@ -1,7 +1,8 @@ use editor::Editor; use gpui::{ - div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton, - MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext, + div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView, + MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, + WindowContext, }; use std::{cmp, sync::Arc}; use ui::{prelude::*, v_stack, Color, Divider, Label}; @@ -16,7 +17,6 @@ pub struct Picker { pub trait PickerDelegate: Sized + 'static { type ListItem: IntoElement; - fn match_count(&self) -> usize; fn selected_index(&self) -> usize; fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext>); @@ -256,3 +256,22 @@ impl Render for Picker { }) } } + +pub fn simple_picker_match( + selected: bool, + cx: &mut WindowContext, + children: impl FnOnce(&mut WindowContext) -> AnyElement, +) -> AnyElement { + let colors = cx.theme().colors(); + + div() + .px_1() + .text_color(colors.text) + .text_ui() + .bg(colors.ghost_element_background) + .rounded_md() + .when(selected, |this| this.bg(colors.ghost_element_selected)) + .hover(|this| this.bg(colors.ghost_element_hover)) + .child((children)(cx)) + .into_any() +} diff --git a/crates/project_panel2/src/file_associations.rs b/crates/project_panel2/src/file_associations.rs index 9e9a865f3e78d2c563476ef4982286c1cfc60405..82aebe7913133d2aa7f673f21387f2c7a8ad3ca3 100644 --- a/crates/project_panel2/src/file_associations.rs +++ b/crates/project_panel2/src/file_associations.rs @@ -41,56 +41,47 @@ impl FileAssociations { }) } - pub fn get_icon(path: &Path, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_icon(path: &Path, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - // FIXME: Associate a type with the languages and have the file's langauge - // override these associations - maybe!({ - let suffix = path.icon_suffix()?; + // FIXME: Associate a type with the languages and have the file's langauge + // override these associations + maybe!({ + let suffix = path.icon_suffix()?; - this.suffixes - .get(suffix) - .and_then(|type_str| this.types.get(type_str)) - .map(|type_config| type_config.icon.clone()) - }) - .or_else(|| this.types.get("default").map(|config| config.icon.clone())) + this.suffixes + .get(suffix) + .and_then(|type_str| this.types.get(type_str)) + .map(|type_config| type_config.icon.clone()) }) - .unwrap_or_else(|| Arc::from("".to_string())) + .or_else(|| this.types.get("default").map(|config| config.icon.clone())) } - pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - let key = if expanded { - EXPANDED_DIRECTORY_TYPE - } else { - COLLAPSED_DIRECTORY_TYPE - }; + let key = if expanded { + EXPANDED_DIRECTORY_TYPE + } else { + COLLAPSED_DIRECTORY_TYPE + }; - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - }) - .unwrap_or_else(|| Arc::from("".to_string())) + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) } - pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc { - maybe!({ - let this = cx.has_global::().then(|| cx.global::())?; + pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option> { + let this = cx.has_global::().then(|| cx.global::())?; - let key = if expanded { - EXPANDED_CHEVRON_TYPE - } else { - COLLAPSED_CHEVRON_TYPE - }; + let key = if expanded { + EXPANDED_CHEVRON_TYPE + } else { + COLLAPSED_CHEVRON_TYPE + }; - this.types - .get(key) - .map(|type_config| type_config.icon.clone()) - }) - .unwrap_or_else(|| Arc::from("".to_string())) + this.types + .get(key) + .map(|type_config| type_config.icon.clone()) } } diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index d4c63e75bf68057bffa8f616f47fa8488e28bedf..359c5a8e6bcebf769ae6a7a3ffa9d11220fd96c9 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -1268,16 +1268,16 @@ impl ProjectPanel { let icon = match entry.kind { EntryKind::File(_) => { if show_file_icons { - Some(FileAssociations::get_icon(&entry.path, cx)) + FileAssociations::get_icon(&entry.path, cx) } else { None } } _ => { if show_folder_icons { - Some(FileAssociations::get_folder_icon(is_expanded, cx)) + FileAssociations::get_folder_icon(is_expanded, cx) } else { - Some(FileAssociations::get_chevron_icon(is_expanded, cx)) + FileAssociations::get_chevron_icon(is_expanded, cx) } } }; diff --git a/crates/theme2/src/registry.rs b/crates/theme2/src/registry.rs index 919dd1b1099ecb35853142a2b5a66f14904a7653..b50eb831dda51b8357ad2b8c8ff9a7b6a86cfe81 100644 --- a/crates/theme2/src/registry.rs +++ b/crates/theme2/src/registry.rs @@ -86,6 +86,10 @@ impl ThemeRegistry { })); } + pub fn clear(&mut self) { + self.themes.clear(); + } + pub fn list_names(&self, _staff: bool) -> impl Iterator + '_ { self.themes.keys().cloned() } diff --git a/crates/theme_selector2/Cargo.toml b/crates/theme_selector2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..853a53af68f25aa0ff12f4cdd7c232b65e3828c8 --- /dev/null +++ b/crates/theme_selector2/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "theme_selector2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/theme_selector.rs" +doctest = false + +[dependencies] +editor = { package = "editor2", path = "../editor2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +fs = { package = "fs2", path = "../fs2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +picker = { package = "picker2", path = "../picker2" } +theme = { package = "theme2", path = "../theme2" } +settings = { package = "settings2", path = "../settings2" } +feature_flags = { package = "feature_flags2", path = "../feature_flags2" } +workspace = { package = "workspace2", path = "../workspace2" } +util = { path = "../util" } +log.workspace = true +parking_lot.workspace = true +postage.workspace = true +smol.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/theme_selector2/src/theme_selector.rs b/crates/theme_selector2/src/theme_selector.rs new file mode 100644 index 0000000000000000000000000000000000000000..ec38f7083cd93f9e61fc88aaa43dd67306621f7c --- /dev/null +++ b/crates/theme_selector2/src/theme_selector.rs @@ -0,0 +1,276 @@ +use feature_flags::FeatureFlagAppExt; +use fs::Fs; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, + SharedString, View, ViewContext, VisualContext, WeakView, +}; +use picker::{Picker, PickerDelegate}; +use settings::{update_settings_file, SettingsStore}; +use std::sync::Arc; +use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings}; +use ui::ListItem; +use util::ResultExt; +use workspace::{ui::HighlightedLabel, Workspace}; + +actions!(Toggle, Reload); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views( + |workspace: &mut Workspace, _cx: &mut ViewContext| { + workspace.register_action(toggle); + }, + ) + .detach(); +} + +pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { + let fs = workspace.app_state().fs.clone(); + workspace.toggle_modal(cx, |cx| { + ThemeSelector::new( + ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx), + cx, + ) + }); +} + +#[cfg(debug_assertions)] +pub fn reload(cx: &mut AppContext) { + let current_theme_name = cx.theme().name.clone(); + let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| { + registry.clear(); + registry.get(¤t_theme_name) + }); + match current_theme { + Ok(theme) => { + ThemeSelectorDelegate::set_theme(theme, cx); + log::info!("reloaded theme {}", current_theme_name); + } + Err(error) => { + log::error!("failed to load theme {}: {:?}", current_theme_name, error) + } + } +} + +pub struct ThemeSelector { + picker: View>, +} + +impl EventEmitter for ThemeSelector {} + +impl FocusableView for ThemeSelector { + fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle { + self.picker.focus_handle(cx) + } +} + +impl Render for ThemeSelector { + type Element = View>; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.picker.clone() + } +} + +impl ThemeSelector { + pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext) -> Self { + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + Self { picker } + } +} + +pub struct ThemeSelectorDelegate { + fs: Arc, + theme_names: Vec, + matches: Vec, + original_theme: Arc, + selection_completed: bool, + selected_index: usize, + view: WeakView, +} + +impl ThemeSelectorDelegate { + fn new( + weak_view: WeakView, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let original_theme = cx.theme().clone(); + + let staff_mode = cx.is_staff(); + let registry = cx.global::>(); + let theme_names = registry.list(staff_mode).collect::>(); + //todo!(theme sorting) + // theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name))); + let matches = theme_names + .iter() + .map(|meta| StringMatch { + candidate_id: 0, + score: 0.0, + positions: Default::default(), + string: meta.to_string(), + }) + .collect(); + let mut this = Self { + fs, + theme_names, + matches, + original_theme: original_theme.clone(), + selected_index: 0, + selection_completed: false, + view: weak_view, + }; + this.select_if_matching(&original_theme.name); + this + } + + fn show_selected_theme(&mut self, cx: &mut ViewContext>) { + if let Some(mat) = self.matches.get(self.selected_index) { + let registry = cx.global::>(); + match registry.get(&mat.string) { + Ok(theme) => { + Self::set_theme(theme, cx); + } + Err(error) => { + log::error!("error loading theme {}: {}", mat.string, error) + } + } + } + } + + fn select_if_matching(&mut self, theme_name: &str) { + self.selected_index = self + .matches + .iter() + .position(|mat| mat.string == theme_name) + .unwrap_or(self.selected_index); + } + + fn set_theme(theme: Arc, cx: &mut AppContext) { + cx.update_global(|store: &mut SettingsStore, cx| { + let mut theme_settings = store.get::(None).clone(); + theme_settings.active_theme = theme; + store.override_global(theme_settings); + cx.refresh(); + }); + } +} + +impl PickerDelegate for ThemeSelectorDelegate { + type ListItem = ui::ListItem; + + fn placeholder_text(&self) -> Arc { + "Select Theme...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + self.selection_completed = true; + + let theme_name = cx.theme().name.clone(); + update_settings_file::(self.fs.clone(), cx, move |settings| { + settings.theme = Some(theme_name.to_string()); + }); + + self.view + .update(cx, |_, cx| { + cx.emit(DismissEvent::Dismiss); + }) + .ok(); + } + + fn dismissed(&mut self, cx: &mut ViewContext>) { + if !self.selection_completed { + Self::set_theme(self.original_theme.clone(), cx); + self.selection_completed = true; + } + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + cx: &mut ViewContext>, + ) { + self.selected_index = ix; + self.show_selected_theme(cx); + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> gpui::Task<()> { + let background = cx.background_executor().clone(); + let candidates = self + .theme_names + .iter() + .enumerate() + .map(|(id, meta)| StringMatchCandidate { + id, + char_bag: meta.as_ref().into(), + string: meta.to_string(), + }) + .collect::>(); + + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, cx| { + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate + .selected_index + .min(this.delegate.matches.len().saturating_sub(1)); + this.delegate.show_selected_theme(cx); + }) + .log_err(); + }) + } + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut ViewContext>, + ) -> Option { + let theme_match = &self.matches[ix]; + + Some( + ListItem::new(ix) + .inset(true) + .selected(selected) + .child(HighlightedLabel::new( + theme_match.string.clone(), + theme_match.positions.clone(), + )), + ) + } +} diff --git a/crates/welcome2/Cargo.toml b/crates/welcome2/Cargo.toml new file mode 100644 index 0000000000000000000000000000000000000000..ce2e102ca09d6efcb241f9a50157946822530c65 --- /dev/null +++ b/crates/welcome2/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "welcome2" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +path = "src/welcome.rs" + +[features] +test-support = [] + +[dependencies] +client = { package = "client2", path = "../client2" } +editor = { package = "editor2", path = "../editor2" } +fs = { package = "fs2", path = "../fs2" } +fuzzy = { package = "fuzzy2", path = "../fuzzy2" } +gpui = { package = "gpui2", path = "../gpui2" } +ui = { package = "ui2", path = "../ui2" } +db = { package = "db2", path = "../db2" } +install_cli = { package = "install_cli2", path = "../install_cli2" } +project = { package = "project2", path = "../project2" } +settings = { package = "settings2", path = "../settings2" } +theme = { package = "theme2", path = "../theme2" } +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } +util = { path = "../util" } +picker = { package = "picker2", path = "../picker2" } +workspace = { package = "workspace2", path = "../workspace2" } +# vim = { package = "vim2", path = "../vim2" } + +anyhow.workspace = true +log.workspace = true +schemars.workspace = true +serde.workspace = true + +[dev-dependencies] +editor = { package = "editor2", path = "../editor2", features = ["test-support"] } diff --git a/crates/welcome2/src/base_keymap_picker.rs b/crates/welcome2/src/base_keymap_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..96195fe2f21e3763bafbadc4c7ca4f7ced58d46a --- /dev/null +++ b/crates/welcome2/src/base_keymap_picker.rs @@ -0,0 +1,208 @@ +use super::base_keymap_setting::BaseKeymap; +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task, + View, ViewContext, VisualContext, WeakView, +}; +use picker::{Picker, PickerDelegate}; +use project::Fs; +use settings::{update_settings_file, Settings}; +use std::sync::Arc; +use ui::ListItem; +use util::ResultExt; +use workspace::{ui::HighlightedLabel, Workspace}; + +actions!(ToggleBaseKeymapSelector); + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(toggle); + }) + .detach(); +} + +pub fn toggle( + workspace: &mut Workspace, + _: &ToggleBaseKeymapSelector, + cx: &mut ViewContext, +) { + let fs = workspace.app_state().fs.clone(); + workspace.toggle_modal(cx, |cx| { + BaseKeymapSelector::new( + BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx), + cx, + ) + }); +} + +pub struct BaseKeymapSelector { + focus_handle: gpui::FocusHandle, + picker: View>, +} + +impl FocusableView for BaseKeymapSelector { + fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl EventEmitter for BaseKeymapSelector {} + +impl BaseKeymapSelector { + pub fn new( + delegate: BaseKeymapSelectorDelegate, + cx: &mut ViewContext, + ) -> Self { + let picker = cx.build_view(|cx| Picker::new(delegate, cx)); + let focus_handle = cx.focus_handle(); + Self { + focus_handle, + picker, + } + } +} + +impl Render for BaseKeymapSelector { + type Element = View>; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + self.picker.clone() + } +} + +pub struct BaseKeymapSelectorDelegate { + view: WeakView, + matches: Vec, + selected_index: usize, + fs: Arc, +} + +impl BaseKeymapSelectorDelegate { + fn new( + weak_view: WeakView, + fs: Arc, + cx: &mut ViewContext, + ) -> Self { + let base = BaseKeymap::get(None, cx); + let selected_index = BaseKeymap::OPTIONS + .iter() + .position(|(_, value)| value == base) + .unwrap_or(0); + Self { + view: weak_view, + matches: Vec::new(), + selected_index, + fs, + } + } +} + +impl PickerDelegate for BaseKeymapSelectorDelegate { + type ListItem = ui::ListItem; + + fn placeholder_text(&self) -> Arc { + "Select a base keymap...".into() + } + + fn match_count(&self) -> usize { + self.matches.len() + } + + fn selected_index(&self) -> usize { + self.selected_index + } + + fn set_selected_index( + &mut self, + ix: usize, + _: &mut ViewContext>, + ) { + self.selected_index = ix; + } + + fn update_matches( + &mut self, + query: String, + cx: &mut ViewContext>, + ) -> Task<()> { + let background = cx.background_executor().clone(); + let candidates = BaseKeymap::names() + .enumerate() + .map(|(id, name)| StringMatchCandidate { + id, + char_bag: name.into(), + string: name.into(), + }) + .collect::>(); + + cx.spawn(|this, mut cx| async move { + let matches = if query.is_empty() { + candidates + .into_iter() + .enumerate() + .map(|(index, candidate)| StringMatch { + candidate_id: index, + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + ) + .await + }; + + this.update(&mut cx, |this, _| { + this.delegate.matches = matches; + this.delegate.selected_index = this + .delegate + .selected_index + .min(this.delegate.matches.len().saturating_sub(1)); + }) + .log_err(); + }) + } + + fn confirm(&mut self, _: bool, cx: &mut ViewContext>) { + if let Some(selection) = self.matches.get(self.selected_index) { + let base_keymap = BaseKeymap::from_names(&selection.string); + update_settings_file::(self.fs.clone(), cx, move |setting| { + *setting = Some(base_keymap) + }); + } + + self.view + .update(cx, |_, cx| { + cx.emit(DismissEvent::Dismiss); + }) + .ok(); + } + + fn dismissed(&mut self, _cx: &mut ViewContext>) {} + + fn render_match( + &self, + ix: usize, + selected: bool, + _cx: &mut gpui::ViewContext>, + ) -> Option { + let keymap_match = &self.matches[ix]; + + Some( + ListItem::new(ix) + .selected(selected) + .inset(true) + .child(HighlightedLabel::new( + keymap_match.string.clone(), + keymap_match.positions.clone(), + )), + ) + } +} diff --git a/crates/welcome2/src/base_keymap_setting.rs b/crates/welcome2/src/base_keymap_setting.rs new file mode 100644 index 0000000000000000000000000000000000000000..cad6e894f95d7c0621f703fa28ed34a6d7726764 --- /dev/null +++ b/crates/welcome2/src/base_keymap_setting.rs @@ -0,0 +1,65 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use settings::Settings; + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] +pub enum BaseKeymap { + #[default] + VSCode, + JetBrains, + SublimeText, + Atom, + TextMate, +} + +impl BaseKeymap { + pub const OPTIONS: [(&'static str, Self); 5] = [ + ("VSCode (Default)", Self::VSCode), + ("Atom", Self::Atom), + ("JetBrains", Self::JetBrains), + ("Sublime Text", Self::SublimeText), + ("TextMate", Self::TextMate), + ]; + + pub fn asset_path(&self) -> Option<&'static str> { + match self { + BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), + BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"), + BaseKeymap::Atom => Some("keymaps/atom.json"), + BaseKeymap::TextMate => Some("keymaps/textmate.json"), + BaseKeymap::VSCode => None, + } + } + + pub fn names() -> impl Iterator { + Self::OPTIONS.iter().map(|(name, _)| *name) + } + + pub fn from_names(option: &str) -> BaseKeymap { + Self::OPTIONS + .iter() + .copied() + .find_map(|(name, value)| (name == option).then(|| value)) + .unwrap_or_default() + } +} + +impl Settings for BaseKeymap { + const KEY: Option<&'static str> = Some("base_keymap"); + + type FileContent = Option; + + fn load( + default_value: &Self::FileContent, + user_values: &[&Self::FileContent], + _: &mut gpui::AppContext, + ) -> anyhow::Result + where + Self: Sized, + { + Ok(user_values + .first() + .and_then(|v| **v) + .unwrap_or(default_value.unwrap())) + } +} diff --git a/crates/welcome2/src/welcome.rs b/crates/welcome2/src/welcome.rs new file mode 100644 index 0000000000000000000000000000000000000000..441c2bf69663084e40d354eccefb8d7bbb66ce49 --- /dev/null +++ b/crates/welcome2/src/welcome.rs @@ -0,0 +1,281 @@ +mod base_keymap_picker; +mod base_keymap_setting; + +use db::kvp::KEY_VALUE_STORE; +use gpui::{ + div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable, + FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View, + ViewContext, VisualContext, WeakView, WindowContext, +}; +use settings::{Settings, SettingsStore}; +use std::sync::Arc; +use workspace::{ + dock::DockPosition, + item::{Item, ItemEvent}, + open_new, AppState, Welcome, Workspace, WorkspaceId, +}; + +pub use base_keymap_setting::BaseKeymap; + +pub const FIRST_OPEN: &str = "first_open"; + +pub fn init(cx: &mut AppContext) { + BaseKeymap::register(cx); + + cx.observe_new_views(|workspace: &mut Workspace, _cx| { + workspace.register_action(|workspace, _: &Welcome, cx| { + let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx)); + workspace.add_item(Box::new(welcome_page), cx) + }); + }) + .detach(); + + base_keymap_picker::init(cx); +} + +pub fn show_welcome_experience(app_state: &Arc, cx: &mut AppContext) { + open_new(&app_state, cx, |workspace, cx| { + workspace.toggle_dock(DockPosition::Left, cx); + let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx)); + workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); + cx.focus_view(&welcome_page); + cx.notify(); + }) + .detach(); + + db::write_and_log(cx, || { + KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string()) + }); +} + +pub struct WelcomePage { + workspace: WeakView, + focus_handle: FocusHandle, + _settings_subscription: Subscription, +} + +impl Render for WelcomePage { + type Element = Focusable
; + + fn render(&mut self, _cx: &mut gpui::ViewContext) -> Self::Element { + // todo!(welcome_ui) + // let self_handle = cx.handle(); + // let theme = cx.theme(); + // let width = theme.welcome.page_width; + + // let telemetry_settings = TelemetrySettings::get(None, cx); + // let vim_mode_setting = VimModeSettings::get(cx); + + div() + .track_focus(&self.focus_handle) + .child(div().size_full().bg(red()).child("Welcome!")) + //todo!() + // PaneBackdrop::new( + // self_handle.id(), + // Flex::column() + // .with_child( + // Flex::column() + // .with_child( + // theme::ui::svg(&theme.welcome.logo) + // .aligned() + // .contained() + // .aligned(), + // ) + // .with_child( + // Label::new( + // "Code at the speed of thought", + // theme.welcome.logo_subheading.text.clone(), + // ) + // .aligned() + // .contained() + // .with_style(theme.welcome.logo_subheading.container), + // ) + // .contained() + // .with_style(theme.welcome.heading_group) + // .constrained() + // .with_width(width), + // ) + // .with_child( + // Flex::column() + // .with_child(theme::ui::cta_button::( + // "Choose a theme", + // width, + // &theme.welcome.button, + // cx, + // |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // theme_selector::toggle(workspace, &Default::default(), cx) + // }) + // } + // }, + // )) + // .with_child(theme::ui::cta_button::( + // "Choose a keymap", + // width, + // &theme.welcome.button, + // cx, + // |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // base_keymap_picker::toggle( + // workspace, + // &Default::default(), + // cx, + // ) + // }) + // } + // }, + // )) + // .with_child(theme::ui::cta_button::( + // "Install the CLI", + // width, + // &theme.welcome.button, + // cx, + // |_, _, cx| { + // cx.app_context() + // .spawn(|cx| async move { install_cli::install_cli(&cx).await }) + // .detach_and_log_err(cx); + // }, + // )) + // .contained() + // .with_style(theme.welcome.button_group) + // .constrained() + // .with_width(width), + // ) + // .with_child( + // Flex::column() + // .with_child( + // theme::ui::checkbox::( + // "Enable vim mode", + // &theme.welcome.checkbox, + // vim_mode_setting, + // 0, + // cx, + // |this, checked, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let fs = workspace.read(cx).app_state().fs.clone(); + // update_settings_file::( + // fs, + // cx, + // move |setting| *setting = Some(checked), + // ) + // } + // }, + // ) + // .contained() + // .with_style(theme.welcome.checkbox_container), + // ) + // .with_child( + // theme::ui::checkbox_with_label::( + // Flex::column() + // .with_child( + // Label::new( + // "Send anonymous usage data", + // theme.welcome.checkbox.label.text.clone(), + // ) + // .contained() + // .with_style(theme.welcome.checkbox.label.container), + // ) + // .with_child( + // Label::new( + // "Help > View Telemetry", + // theme.welcome.usage_note.text.clone(), + // ) + // .contained() + // .with_style(theme.welcome.usage_note.container), + // ), + // &theme.welcome.checkbox, + // telemetry_settings.metrics, + // 0, + // cx, + // |this, checked, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let fs = workspace.read(cx).app_state().fs.clone(); + // update_settings_file::( + // fs, + // cx, + // move |setting| setting.metrics = Some(checked), + // ) + // } + // }, + // ) + // .contained() + // .with_style(theme.welcome.checkbox_container), + // ) + // .with_child( + // theme::ui::checkbox::( + // "Send crash reports", + // &theme.welcome.checkbox, + // telemetry_settings.diagnostics, + // 1, + // cx, + // |this, checked, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let fs = workspace.read(cx).app_state().fs.clone(); + // update_settings_file::( + // fs, + // cx, + // move |setting| setting.diagnostics = Some(checked), + // ) + // } + // }, + // ) + // .contained() + // .with_style(theme.welcome.checkbox_container), + // ) + // .contained() + // .with_style(theme.welcome.checkbox_group) + // .constrained() + // .with_width(width), + // ) + // .constrained() + // .with_max_width(width) + // .contained() + // .with_uniform_padding(10.) + // .aligned() + // .into_any(), + // ) + // .into_any_named("welcome page") + } +} + +impl WelcomePage { + pub fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + WelcomePage { + focus_handle: cx.focus_handle(), + workspace: workspace.weak_handle(), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + } + } +} + +impl EventEmitter for WelcomePage {} + +impl FocusableView for WelcomePage { + fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} + +impl Item for WelcomePage { + fn tab_content(&self, _: Option, _: &WindowContext) -> AnyElement { + "Welcome to Zed!".into_any() + } + + fn show_toolbar(&self) -> bool { + false + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> { + Some(cx.build_view(|cx| WelcomePage { + focus_handle: cx.focus_handle(), + workspace: self.workspace.clone(), + _settings_subscription: cx.observe_global::(move |_, cx| cx.notify()), + })) + } +} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 6fa722f7757615ad01acdddfc8d527f457f94411..0252d868b8cc1c5f06e8adfa77112a3875448ee5 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -1808,22 +1808,22 @@ impl Workspace { pane } - // pub fn add_item_to_center( - // &mut self, - // item: Box, - // cx: &mut ViewContext, - // ) -> bool { - // if let Some(center_pane) = self.last_active_center_pane.clone() { - // if let Some(center_pane) = center_pane.upgrade(cx) { - // center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); - // true - // } else { - // false - // } - // } else { - // false - // } - // } + pub fn add_item_to_center( + &mut self, + item: Box, + cx: &mut ViewContext, + ) -> bool { + if let Some(center_pane) = self.last_active_center_pane.clone() { + if let Some(center_pane) = center_pane.upgrade() { + center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx)); + true + } else { + false + } + } else { + false + } + } pub fn add_item(&mut self, item: Box, cx: &mut ViewContext) { self.active_pane diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index dd08f7bd2f055470506989bf275fa60c3248091d..3f4fb5c7d8d4c7b7db7e469e7d473ed1cdf4fa47 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -66,12 +66,12 @@ shellexpand = "2.1.0" text = { package = "text2", path = "../text2" } terminal_view = { package = "terminal_view2", path = "../terminal_view2" } theme = { package = "theme2", path = "../theme2" } -# theme_selector = { path = "../theme_selector" } +theme_selector = { package = "theme_selector2", path = "../theme_selector2" } util = { path = "../util" } # semantic_index = { path = "../semantic_index" } # vim = { path = "../vim" } workspace = { package = "workspace2", path = "../workspace2" } -# welcome = { path = "../welcome" } +welcome = { package = "welcome2", path = "../welcome2" } zed_actions = {package = "zed_actions2", path = "../zed_actions2"} anyhow.workspace = true async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index 7b51c894fefe9f1cee1a9d741ec883f77e956791..493988c945f8f7ef9f32e9f17de3bf3e8522ce72 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE; use editor::Editor; use fs::RealFs; use futures::StreamExt; -use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; +use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task}; use isahc::{prelude::Configurable, Request}; use language::LanguageRegistry; use log::LevelFilter; @@ -36,7 +36,7 @@ use std::{ path::{Path, PathBuf}, sync::{ atomic::{AtomicU32, Ordering}, - Arc, + Arc, Weak, }, thread, }; @@ -48,6 +48,7 @@ use util::{ paths, ResultExt, }; use uuid::Uuid; +use welcome::{show_welcome_experience, FIRST_OPEN}; use workspace::{AppState, WorkspaceStore}; use zed2::{ build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace, @@ -103,16 +104,15 @@ fn main() { let listener = Arc::new(listener); let open_listener = listener.clone(); app.on_open_urls(move |urls, _| open_listener.open_urls(&urls)); - app.on_reopen(move |_cx| { - // todo!("workspace") - // if cx.has_global::>() { - // if let Some(app_state) = cx.global::>().upgrade() { - // workspace::open_new(&app_state, cx, |workspace, cx| { - // Editor::new_file(workspace, &Default::default(), cx) - // }) - // .detach(); - // } - // } + app.on_reopen(move |cx| { + if cx.has_global::>() { + if let Some(app_state) = cx.global::>().upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } + } }); app.run(move |cx| { @@ -164,17 +164,16 @@ fn main() { // assistant::init(cx); // component_test::init(cx); - // cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach(); // cx.spawn(|_| watch_languages(fs.clone(), languages.clone())) // .detach(); - // watch_file_types(fs.clone(), cx); + watch_file_types(fs.clone(), cx); languages.set_theme(cx.theme().clone()); - // cx.observe_global::({ - // let languages = languages.clone(); - // move |cx| languages.set_theme(theme::current(cx).clone()) - // }) - // .detach(); + cx.observe_global::({ + let languages = languages.clone(); + move |cx| languages.set_theme(cx.theme().clone()) + }) + .detach(); client.telemetry().start(installation_id, session_id, cx); let telemetry_settings = *client::TelemetrySettings::get_global(cx); @@ -193,7 +192,6 @@ fn main() { fs, build_window_options, call_factory: call::Call::new, - // background_actions: todo!("ask Mikayla"), workspace_store, node_runtime, }); @@ -219,14 +217,13 @@ fn main() { // journal2::init(app_state.clone(), cx); // language_selector::init(cx); - // theme_selector::init(cx); + theme_selector::init(cx); // activity_indicator::init(cx); // language_tools::init(cx); call::init(app_state.client.clone(), app_state.user_store.clone(), cx); collab_ui::init(&app_state, cx); // feedback::init(cx); - // welcome::init(cx); - // zed::init(&app_state, cx); + welcome::init(cx); // cx.set_menus(menus::menus()); initialize_workspace(app_state.clone(), cx); @@ -279,17 +276,18 @@ fn main() { .detach(); } Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => { - todo!() - // triggered_authentication = true; - // let app_state = app_state.clone(); - // let client = client.clone(); - // cx.spawn(|mut cx| async move { - // // ignore errors here, we'll show a generic "not signed in" - // let _ = authenticate(client, &cx).await; - // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) - // .await - // }) - // .detach_and_log_err(cx) + triggered_authentication = true; + let app_state = app_state.clone(); + let client = client.clone(); + cx.spawn(|mut cx| async move { + // ignore errors here, we'll show a generic "not signed in" + let _ = authenticate(client, &cx).await; + //todo!() + // cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx)) + // .await + anyhow::Ok(()) + }) + .detach_and_log_err(cx) } Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => { todo!() @@ -340,7 +338,7 @@ async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { if client::IMPERSONATE_LOGIN.is_some() { client.authenticate_and_connect(false, &cx).await?; } - } else if client.has_keychain_credentials(&cx).await { + } else if client.has_keychain_credentials(&cx) { client.authenticate_and_connect(true, &cx).await?; } Ok::<_, anyhow::Error>(()) @@ -368,10 +366,9 @@ async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncApp cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))? .await .log_err(); - // todo!(welcome) - //} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { - //todo!() - // cx.update(|cx| show_welcome_experience(app_state, cx)); + } else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) { + cx.update(|cx| show_welcome_experience(app_state, cx)) + .log_err(); } else { cx.update(|cx| { workspace::open_new(app_state, cx, |workspace, cx| { @@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) { .unwrap(); } -// #[cfg(debug_assertions)] -// async fn watch_themes(fs: Arc, mut cx: AsyncAppContext) -> Option<()> { -// let mut events = fs -// .watch("styles/src".as_ref(), Duration::from_millis(100)) -// .await; -// while (events.next().await).is_some() { -// let output = Command::new("npm") -// .current_dir("styles") -// .args(["run", "build"]) -// .output() -// .await -// .log_err()?; -// if output.status.success() { -// cx.update(|cx| theme_selector::reload(cx)) -// } else { -// eprintln!( -// "build script failed {}", -// String::from_utf8_lossy(&output.stderr) -// ); -// } -// } -// Some(()) -// } - -// #[cfg(debug_assertions)] -// async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { -// let mut events = fs -// .watch( -// "crates/zed/src/languages".as_ref(), -// Duration::from_millis(100), -// ) -// .await; -// while (events.next().await).is_some() { -// languages.reload(); -// } -// Some(()) -// } - -// #[cfg(debug_assertions)] -// fn watch_file_types(fs: Arc, cx: &mut AppContext) { -// cx.spawn(|mut cx| async move { -// let mut events = fs -// .watch( -// "assets/icons/file_icons/file_types.json".as_ref(), -// Duration::from_millis(100), -// ) -// .await; -// while (events.next().await).is_some() { -// cx.update(|cx| { -// cx.update_global(|file_types, _| { -// *file_types = project_panel::file_associations::FileAssociations::new(Assets); -// }); -// }) -// } -// }) -// .detach() -// } - -// #[cfg(not(debug_assertions))] -// async fn watch_themes(_fs: Arc, _cx: AsyncAppContext) -> Option<()> { -// None -// } - -// #[cfg(not(debug_assertions))] -// async fn watch_languages(_: Arc, _: Arc) -> Option<()> { -// None -// - -// #[cfg(not(debug_assertions))] -// fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} - -pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { - // &[ - // ("Go to file", &file_finder::Toggle), - // ("Open command palette", &command_palette::Toggle), - // ("Open recent projects", &recent_projects::OpenRecent), - // ("Change your settings", &zed_actions::OpenSettings), - // ] - // todo!() - &[] +#[cfg(debug_assertions)] +async fn watch_languages(fs: Arc, languages: Arc) -> Option<()> { + use std::time::Duration; + + let mut events = fs + .watch( + "crates/zed2/src/languages".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + languages.reload(); + } + Some(()) +} + +#[cfg(debug_assertions)] +fn watch_file_types(fs: Arc, cx: &mut AppContext) { + use std::time::Duration; + + cx.spawn(|mut cx| async move { + let mut events = fs + .watch( + "assets/icons/file_icons/file_types.json".as_ref(), + Duration::from_millis(100), + ) + .await; + while (events.next().await).is_some() { + cx.update(|cx| { + cx.update_global(|file_types, _| { + *file_types = project_panel::file_associations::FileAssociations::new(Assets); + }); + }) + .ok(); + } + }) + .detach() } + +#[cfg(not(debug_assertions))] +async fn watch_languages(_: Arc, _: Arc) -> Option<()> { + None +} + +#[cfg(not(debug_assertions))] +fn watch_file_types(_fs: Arc, _cx: &mut AppContext) {} diff --git a/script/crate-dep-graph b/script/crate-dep-graph index 25285cc097c01331e36ea34a9b5a9ef622f43342..74ea36683cde45a4453b7ee72531a4b83eef1b60 100755 --- a/script/crate-dep-graph +++ b/script/crate-dep-graph @@ -11,7 +11,7 @@ graph_file=target/crate-graph.html cargo depgraph \ --workspace-only \ --offline \ - --root=zed,cli,collab \ + --root=zed2,cli,collab2 \ --dedup-transitive-deps \ | dot -Tsvg > $graph_file