WIP

Mikayla created

Change summary

Cargo.lock                                   |  46 +++
Cargo.toml                                   |   2 
crates/client2/src/client2.rs                |   8 
crates/gpui2/src/action.rs                   |   1 
crates/theme2/src/registry.rs                |   4 
crates/theme_selector2/Cargo.toml            |  28 ++
crates/theme_selector2/src/theme_selector.rs | 254 +++++++++++++++++++
crates/welcome2/Cargo.toml                   |  36 ++
crates/welcome2/src/base_keymap_picker.rs    | 152 +++++++++++
crates/welcome2/src/base_keymap_setting.rs   |  65 ++++
crates/welcome2/src/welcome.rs               | 287 ++++++++++++++++++++++
crates/zed2/Cargo.toml                       |   4 
crates/zed2/src/main.rs                      |  83 ++---
script/crate-dep-graph                       |   2 
14 files changed, 921 insertions(+), 51 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -9405,6 +9405,26 @@ 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",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "thiserror"
 version = "1.0.48"
@@ -10978,6 +10998,30 @@ 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",
+ "util",
+ "workspace2",
+]
+
 [[package]]
 name = "which"
 version = "4.4.2"
@@ -11640,6 +11684,7 @@ dependencies = [
  "terminal_view2",
  "text2",
  "theme2",
+ "theme_selector2",
  "thiserror",
  "tiny_http",
  "toml 0.5.11",
@@ -11676,6 +11721,7 @@ dependencies = [
  "urlencoding",
  "util",
  "uuid 1.4.1",
+ "welcome2",
  "workspace2",
  "zed_actions2",
 ]

Cargo.toml 🔗

@@ -108,6 +108,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",

crates/client2/src/client2.rs 🔗

@@ -694,8 +694,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)]
@@ -726,7 +726,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() {
@@ -1325,7 +1325,7 @@ impl Client {
     }
 }
 
-async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
+fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
     if IMPERSONATE_LOGIN.is_some() {
         return None;
     }

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;
     };
 

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<Item = SharedString> + '_ {
         self.themes.keys().cloned()
     }

crates/theme_selector2/Cargo.toml 🔗

@@ -0,0 +1,28 @@
+[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" }
+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"] }

crates/theme_selector2/src/theme_selector.rs 🔗

@@ -0,0 +1,254 @@
+use feature_flags::FeatureFlagAppExt;
+use fs::Fs;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions, div, AppContext, Div, EventEmitter, FocusableView, Manager, Render, SharedString,
+    View, ViewContext, VisualContext,
+};
+use picker::{Picker, PickerDelegate};
+use settings::{update_settings_file, SettingsStore};
+use std::sync::Arc;
+use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
+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>| {
+            workspace.register_action(toggle);
+        },
+    );
+}
+
+pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
+    let fs = workspace.app_state().fs.clone();
+    workspace.toggle_modal(cx, |cx| {
+        ThemeSelector::new(ThemeSelectorDelegate::new(fs, cx), cx)
+    });
+}
+
+#[cfg(debug_assertions)]
+pub fn reload(cx: &mut AppContext) {
+    let current_theme_name = cx.theme().name.clone();
+    let registry = cx.global::<Arc<ThemeRegistry>>();
+    registry.clear();
+    match registry.get(&current_theme_name) {
+        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<Picker<ThemeSelectorDelegate>>,
+}
+
+impl EventEmitter<Manager> 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<Picker<ThemeSelectorDelegate>>;
+
+    fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+        self.picker.clone()
+    }
+}
+
+impl ThemeSelector {
+    pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
+        let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+        Self { picker }
+    }
+}
+
+pub struct ThemeSelectorDelegate {
+    fs: Arc<dyn Fs>,
+    theme_names: Vec<SharedString>,
+    matches: Vec<StringMatch>,
+    original_theme: Arc<Theme>,
+    selection_completed: bool,
+    selected_index: usize,
+}
+
+impl ThemeSelectorDelegate {
+    fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<ThemeSelector>) -> Self {
+        let original_theme = cx.theme().clone();
+
+        let staff_mode = cx.is_staff();
+        let registry = cx.global::<Arc<ThemeRegistry>>();
+        let mut theme_names = registry.list(staff_mode).collect::<Vec<_>>();
+        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,
+        };
+        this.select_if_matching(&original_theme.meta.name);
+        this
+    }
+
+    fn show_selected_theme(&mut self, cx: &mut ViewContext<ThemeSelector>) {
+        if let Some(mat) = self.matches.get(self.selected_index) {
+            let registry = cx.global::<Arc<ThemeRegistry>>();
+            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<Theme>, cx: &mut AppContext) {
+        cx.update_global::<SettingsStore, _, _>(|store, cx| {
+            let mut theme_settings = store.get::<ThemeSettings>(None).clone();
+            theme_settings.theme = theme;
+            store.override_global(theme_settings);
+            cx.refresh_windows();
+        });
+    }
+}
+
+impl PickerDelegate for ThemeSelectorDelegate {
+    type ListItem = Div;
+
+    fn placeholder_text(&self) -> Arc<str> {
+        "Select Theme...".into()
+    }
+
+    fn match_count(&self) -> usize {
+        self.matches.len()
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<ThemeSelector>) {
+        self.selection_completed = true;
+
+        let theme_name = cx.theme().meta.name.clone();
+        update_settings_file::<ThemeSettings>(self.fs.clone(), cx, |settings| {
+            settings.theme = Some(theme_name);
+        });
+
+        cx.emit(Manager::Dismiss);
+    }
+
+    fn dismissed(&mut self, cx: &mut ViewContext<ThemeSelector>) {
+        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<ThemeSelector>) {
+        self.selected_index = ix;
+        self.show_selected_theme(cx);
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<ThemeSelector>,
+    ) -> gpui::Task<()> {
+        let background = cx.background().clone();
+        let candidates = self
+            .theme_names
+            .iter()
+            .enumerate()
+            .map(|(id, meta)| StringMatchCandidate {
+                id,
+                char_bag: meta.name.as_str().into(),
+                string: meta.name.clone(),
+            })
+            .collect::<Vec<_>>();
+
+        cx.spawn(|this, mut cx| async move {
+            let matches = if query.is_empty() {
+                candidates
+                    .into_iter()
+                    .enumerate()
+                    .map(|(index, candidate)| StringMatch {
+                        candidate_id: index,
+                        string: candidate.string,
+                        positions: Vec::new(),
+                        score: 0.0,
+                    })
+                    .collect()
+            } else {
+                match_strings(
+                    &candidates,
+                    &query,
+                    false,
+                    100,
+                    &Default::default(),
+                    background,
+                )
+                .await
+            };
+
+            this.update(&mut cx, |this, cx| {
+                let delegate = this.delegate_mut();
+                delegate.matches = matches;
+                delegate.selected_index = delegate
+                    .selected_index
+                    .min(delegate.matches.len().saturating_sub(1));
+                delegate.show_selected_theme(cx);
+            })
+            .log_err();
+        })
+    }
+
+    fn render_match(&self, ix: usize, selected: bool, cx: &AppContext) -> Self::ListItem {
+        let theme = cx.theme();
+        let colors = theme.colors();
+
+        let theme_match = &self.matches[ix];
+        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(HighlightedLabel::new(
+                theme_match.string.clone(),
+                theme_match.positions.clone(),
+            ))
+    }
+}

crates/welcome2/Cargo.toml 🔗

@@ -0,0 +1,36 @@
+[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" }
+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"] }

crates/welcome2/src/base_keymap_picker.rs 🔗

@@ -0,0 +1,152 @@
+use super::base_keymap_setting::BaseKeymap;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+    actions,
+    elements::{Element as _, Label},
+    AppContext, Task, ViewContext,
+};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use project::Fs;
+use settings::update_settings_file;
+use std::sync::Arc;
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(welcome, [ToggleBaseKeymapSelector]);
+
+pub fn init(cx: &mut AppContext) {
+    cx.add_action(toggle);
+    BaseKeymapSelector::init(cx);
+}
+
+pub fn toggle(
+    workspace: &mut Workspace,
+    _: &ToggleBaseKeymapSelector,
+    cx: &mut ViewContext<Workspace>,
+) {
+    workspace.toggle_modal(cx, |workspace, cx| {
+        let fs = workspace.app_state().fs.clone();
+        cx.add_view(|cx| BaseKeymapSelector::new(BaseKeymapSelectorDelegate::new(fs, cx), cx))
+    });
+}
+
+pub type BaseKeymapSelector = Picker<BaseKeymapSelectorDelegate>;
+
+pub struct BaseKeymapSelectorDelegate {
+    matches: Vec<StringMatch>,
+    selected_index: usize,
+    fs: Arc<dyn Fs>,
+}
+
+impl BaseKeymapSelectorDelegate {
+    fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<BaseKeymapSelector>) -> Self {
+        let base = settings::get::<BaseKeymap>(cx);
+        let selected_index = BaseKeymap::OPTIONS
+            .iter()
+            .position(|(_, value)| value == base)
+            .unwrap_or(0);
+        Self {
+            matches: Vec::new(),
+            selected_index,
+            fs,
+        }
+    }
+}
+
+impl PickerDelegate for BaseKeymapSelectorDelegate {
+    fn placeholder_text(&self) -> Arc<str> {
+        "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<BaseKeymapSelector>) {
+        self.selected_index = ix;
+    }
+
+    fn update_matches(
+        &mut self,
+        query: String,
+        cx: &mut ViewContext<BaseKeymapSelector>,
+    ) -> Task<()> {
+        let background = cx.background().clone();
+        let candidates = BaseKeymap::names()
+            .enumerate()
+            .map(|(id, name)| StringMatchCandidate {
+                id,
+                char_bag: name.into(),
+                string: name.into(),
+            })
+            .collect::<Vec<_>>();
+
+        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, _| {
+                let delegate = this.delegate_mut();
+                delegate.matches = matches;
+                delegate.selected_index = delegate
+                    .selected_index
+                    .min(delegate.matches.len().saturating_sub(1));
+            })
+            .log_err();
+        })
+    }
+
+    fn confirm(&mut self, _: bool, cx: &mut ViewContext<BaseKeymapSelector>) {
+        if let Some(selection) = self.matches.get(self.selected_index) {
+            let base_keymap = BaseKeymap::from_names(&selection.string);
+            update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
+                *setting = Some(base_keymap)
+            });
+        }
+        cx.emit(PickerEvent::Dismiss);
+    }
+
+    fn dismissed(&mut self, _cx: &mut ViewContext<BaseKeymapSelector>) {}
+
+    fn render_match(
+        &self,
+        ix: usize,
+        mouse_state: &mut gpui::MouseState,
+        selected: bool,
+        cx: &gpui::AppContext,
+    ) -> gpui::AnyElement<Picker<Self>> {
+        let theme = &theme::current(cx);
+        let keymap_match = &self.matches[ix];
+        let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+
+        Label::new(keymap_match.string.clone(), style.label.clone())
+            .with_highlights(keymap_match.positions.clone())
+            .contained()
+            .with_style(style.container)
+            .into_any()
+    }
+}

crates/welcome2/src/base_keymap_setting.rs 🔗

@@ -0,0 +1,65 @@
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::Setting;
+
+#[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<Item = &'static str> {
+        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 Setting for BaseKeymap {
+    const KEY: Option<&'static str> = Some("base_keymap");
+
+    type FileContent = Option<Self>;
+
+    fn load(
+        default_value: &Self::FileContent,
+        user_values: &[&Self::FileContent],
+        _: &gpui::AppContext,
+    ) -> anyhow::Result<Self>
+    where
+        Self: Sized,
+    {
+        Ok(user_values
+            .first()
+            .and_then(|v| **v)
+            .unwrap_or(default_value.unwrap()))
+    }
+}

crates/welcome2/src/welcome.rs 🔗

@@ -0,0 +1,287 @@
+mod base_keymap_picker;
+mod base_keymap_setting;
+
+use crate::base_keymap_picker::ToggleBaseKeymapSelector;
+use client::TelemetrySettings;
+use db::kvp::KEY_VALUE_STORE;
+use gpui::{
+    elements::{Flex, Label, ParentElement},
+    AnyElement, AppContext, Element, Entity, Subscription, View, ViewContext, WeakViewHandle,
+};
+use settings::{update_settings_file, SettingsStore};
+use std::{borrow::Cow, sync::Arc};
+use vim::VimModeSetting;
+use workspace::{
+    dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace,
+    WorkspaceId,
+};
+
+pub use base_keymap_setting::BaseKeymap;
+
+pub const FIRST_OPEN: &str = "first_open";
+
+pub fn init(cx: &mut AppContext) {
+    settings::register::<BaseKeymap>(cx);
+
+    cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| {
+        let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
+        workspace.add_item(Box::new(welcome_page), cx)
+    });
+
+    base_keymap_picker::init(cx);
+}
+
+pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
+    open_new(&app_state, cx, |workspace, cx| {
+        workspace.toggle_dock(DockPosition::Left, cx);
+        let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx));
+        workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
+        cx.focus(&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: WeakViewHandle<Workspace>,
+    _settings_subscription: Subscription,
+}
+
+impl Entity for WelcomePage {
+    type Event = ();
+}
+
+impl View for WelcomePage {
+    fn ui_name() -> &'static str {
+        "WelcomePage"
+    }
+
+    fn render(&mut self, cx: &mut gpui::ViewContext<Self>) -> AnyElement<Self> {
+        let self_handle = cx.handle();
+        let theme = theme::current(cx);
+        let width = theme.welcome.page_width;
+
+        let telemetry_settings = *settings::get::<TelemetrySettings>(cx);
+        let vim_mode_setting = settings::get::<VimModeSetting>(cx).0;
+
+        enum Metrics {}
+        enum Diagnostics {}
+
+        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::<theme_selector::Toggle, _, _, _>(
+                            "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::<ToggleBaseKeymapSelector, _, _, _>(
+                            "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_cli::Install, _, _, _>(
+                            "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::<Diagnostics, Self, _>(
+                                "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::<VimModeSetting>(
+                                            fs,
+                                            cx,
+                                            move |setting| *setting = Some(checked),
+                                        )
+                                    }
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container),
+                        )
+                        .with_child(
+                            theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
+                                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::<TelemetrySettings>(
+                                            fs,
+                                            cx,
+                                            move |setting| setting.metrics = Some(checked),
+                                        )
+                                    }
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container),
+                        )
+                        .with_child(
+                            theme::ui::checkbox::<Diagnostics, Self, _>(
+                                "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::<TelemetrySettings>(
+                                            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>) -> Self {
+        WelcomePage {
+            workspace: workspace.weak_handle(),
+            _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
+        }
+    }
+}
+
+impl Item for WelcomePage {
+    fn tab_tooltip_text(&self, _: &AppContext) -> Option<Cow<str>> {
+        Some("Welcome to Zed!".into())
+    }
+
+    fn tab_content<T: 'static>(
+        &self,
+        _detail: Option<usize>,
+        style: &theme::Tab,
+        _cx: &gpui::AppContext,
+    ) -> AnyElement<T> {
+        Flex::row()
+            .with_child(
+                Label::new("Welcome to Zed!", style.label.clone())
+                    .aligned()
+                    .contained(),
+            )
+            .into_any()
+    }
+
+    fn show_toolbar(&self) -> bool {
+        false
+    }
+
+    fn clone_on_split(
+        &self,
+        _workspace_id: WorkspaceId,
+        cx: &mut ViewContext<Self>,
+    ) -> Option<Self> {
+        Some(WelcomePage {
+            workspace: self.workspace.clone(),
+            _settings_subscription: cx.observe_global::<SettingsStore, _>(move |_, cx| cx.notify()),
+        })
+    }
+}

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"] }

crates/zed2/src/main.rs 🔗

@@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
 use backtrace::Backtrace;
 use chrono::Utc;
 use cli::FORCE_CLI_MODE_ENV_VAR_NAME;
-use client::UserStore;
+use client::{Client, UserStore};
 use db::kvp::KEY_VALUE_STORE;
 use editor::Editor;
 use fs::RealFs;
@@ -36,7 +36,7 @@ use std::{
     path::{Path, PathBuf},
     sync::{
         atomic::{AtomicU32, Ordering},
-        Arc,
+        Arc, Weak,
     },
     thread,
 };
@@ -99,16 +99,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::<Weak<AppState>>() {
-        // if let Some(app_state) = cx.global::<Weak<AppState>>().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::<Weak<AppState>>() {
+            if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
+                workspace::open_new(&app_state, cx, |workspace, cx| {
+                    Editor::new_file(workspace, &Default::default(), cx)
+                })
+                .detach();
+            }
+        }
     });
 
     app.run(move |cx| {
@@ -180,7 +179,6 @@ fn main() {
             user_store,
             fs,
             build_window_options,
-            // background_actions: todo!("ask Mikayla"),
             workspace_store,
             node_runtime,
         });
@@ -236,7 +234,7 @@ fn main() {
             }
         }
 
-        let mut _triggered_authentication = false;
+        let mut triggered_authentication = false;
 
         fn open_paths_and_log_errs(
             paths: &[PathBuf],
@@ -266,17 +264,17 @@ 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;
+                    // 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!()
@@ -315,23 +313,23 @@ fn main() {
         })
         .detach();
 
-        // if !triggered_authentication {
-        //     cx.spawn(|cx| async move { authenticate(client, &cx).await })
-        //         .detach_and_log_err(cx);
-        // }
+        if !triggered_authentication {
+            cx.spawn(|cx| async move { authenticate(client, &cx).await })
+                .detach_and_log_err(cx);
+        }
     });
 }
 
-// async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
-//     if stdout_is_a_pty() {
-//         if client::IMPERSONATE_LOGIN.is_some() {
-//             client.authenticate_and_connect(false, &cx).await?;
-//         }
-//     } else if client.has_keychain_credentials(&cx) {
-//         client.authenticate_and_connect(true, &cx).await?;
-//     }
-//     Ok::<_, anyhow::Error>(())
-// }
+async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
+    if stdout_is_a_pty() {
+        if client::IMPERSONATE_LOGIN.is_some() {
+            client.authenticate_and_connect(false, &cx).await?;
+        }
+    } else if client.has_keychain_credentials(&cx) {
+        client.authenticate_and_connect(true, &cx).await?;
+    }
+    Ok::<_, anyhow::Error>(())
+}
 
 async fn installation_id() -> Result<String> {
     let legacy_key_name = "device_id";
@@ -355,11 +353,8 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
             cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
                 .await
                 .log_err();
-        } else if matches!(KEY_VALUE_STORE.read_kvp("******* THIS IS A BAD KEY PLEASE UNCOMMENT BELOW TO FIX THIS VERY LONG LINE *******"), Ok(None)) {
-            // 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));
         } else {
             cx.update(|cx| {
                 workspace::open_new(app_state, cx, |workspace, cx| {

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