From 350ddf20257e29bb9e4a55dbf2e380b703557927 Mon Sep 17 00:00:00 2001 From: Mikayla Maki Date: Tue, 7 Mar 2023 17:13:01 -0800 Subject: [PATCH] Add keymap picker UI Co-authored-by: Max --- Cargo.lock | 2 + crates/project_panel/src/project_panel.rs | 2 +- crates/settings/src/keymap_file.rs | 4 +- crates/settings/src/settings.rs | 46 ++++-- crates/welcome/Cargo.toml | 2 + crates/welcome/src/base_keymap_picker.rs | 175 ++++++++++++++++++++++ crates/welcome/src/welcome.rs | 17 ++- crates/zed/src/zed.rs | 3 +- 8 files changed, 225 insertions(+), 26 deletions(-) create mode 100644 crates/welcome/src/base_keymap_picker.rs diff --git a/Cargo.lock b/Cargo.lock index 2b894eff35..9d950712a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8030,9 +8030,11 @@ version = "0.1.0" dependencies = [ "anyhow", "editor", + "fuzzy", "gpui", "install_cli", "log", + "picker", "project", "settings", "theme", diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 5de68b12fd..a95a8b2deb 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1329,7 +1329,7 @@ impl View for ProjectPanel { keystroke_label( parent_view_id, - "Open a new project", + "Open project", &button_style, context_menu_item.keystroke, workspace::Open, diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index 9048719d3f..2235bc351d 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -46,8 +46,8 @@ impl KeymapFileContent { Self::load(path, cx).unwrap(); } - if let Some(base_keymap) = cx.global::().base_keymap { - Self::load(base_keymap.asset_path(), cx).log_err(); + if let Some(asset_path) = cx.global::().base_keymap.asset_path() { + Self::load(asset_path, cx).log_err(); } } diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index ae67428948..49f43e7a2d 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -55,24 +55,46 @@ pub struct Settings { pub telemetry_defaults: TelemetrySettings, pub telemetry_overrides: TelemetrySettings, pub auto_update: bool, - pub base_keymap: Option, + pub base_keymap: BaseKeymap, } -#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)] pub enum BaseKeymap { + #[default] + VSCode, JetBrains, Sublime, Atom, } impl BaseKeymap { - pub fn asset_path(&self) -> &str { + pub const OPTIONS: [(&'static str, Self); 4] = [ + ("VSCode (Default)", Self::VSCode), + ("Atom", Self::Atom), + ("JetBrains", Self::JetBrains), + ("Sublime", Self::Sublime), + ]; + + pub fn asset_path(&self) -> Option<&'static str> { match self { - BaseKeymap::JetBrains => "keymaps/jetbrains.json", - BaseKeymap::Sublime => "keymaps/sublime_text.json", - BaseKeymap::Atom => "keymaps/atom.json", + BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"), + BaseKeymap::Sublime => Some("keymaps/sublime_text.json"), + BaseKeymap::Atom => Some("keymaps/atom.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() + } } #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -455,7 +477,7 @@ impl Settings { merge(&mut self.vim_mode, data.vim_mode); merge(&mut self.autosave, data.autosave); merge(&mut self.default_dock_anchor, data.default_dock_anchor); - merge(&mut self.base_keymap, Some(data.base_keymap)); + merge(&mut self.base_keymap, data.base_keymap); // Ensure terminal font is loaded, so we can request it in terminal_element layout if let Some(terminal_font) = &data.terminal.font_family { @@ -633,7 +655,7 @@ impl Settings { }, telemetry_overrides: Default::default(), auto_update: true, - base_keymap: None, + base_keymap: Default::default(), } } @@ -722,13 +744,7 @@ pub fn parse_json_with_comments(content: &str) -> Result )?) } -/// Expects the key to be unquoted, and the value to be valid JSON -/// (e.g. values should be unquoted for numbers and bools, quoted for strings) -pub fn write_settings_key( - settings_content: &mut String, - key_path: &[&str], - new_value: &T, -) { +fn write_settings_key(settings_content: &mut String, key_path: &[&str], new_value: &Value) { let mut parser = tree_sitter::Parser::new(); parser.set_language(tree_sitter_json::language()).unwrap(); let tree = parser.parse(&settings_content, None).unwrap(); diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index e76636411b..d3b0e09697 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -14,6 +14,7 @@ test-support = [] anyhow = "1.0.38" log = "0.4" editor = { path = "../editor" } +fuzzy = { path = "../fuzzy" } gpui = { path = "../gpui" } install_cli = { path = "../install_cli" } project = { path = "../project" } @@ -21,4 +22,5 @@ settings = { path = "../settings" } theme = { path = "../theme" } theme_selector = { path = "../theme_selector" } util = { path = "../util" } +picker = { path = "../picker" } workspace = { path = "../workspace" } \ No newline at end of file diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs new file mode 100644 index 0000000000..a37bcb1837 --- /dev/null +++ b/crates/welcome/src/base_keymap_picker.rs @@ -0,0 +1,175 @@ +use fuzzy::{match_strings, StringMatch, StringMatchCandidate}; +use gpui::{ + actions, + elements::{ChildView, Element as _, Label}, + AnyViewHandle, Entity, MutableAppContext, View, ViewContext, ViewHandle, +}; +use picker::{Picker, PickerDelegate}; +use settings::{settings_file::SettingsFile, BaseKeymap, Settings}; +use workspace::Workspace; + +pub struct BaseKeymapSelector { + matches: Vec, + picker: ViewHandle>, + selected_index: usize, +} + +actions!(welcome, [ToggleBaseKeymapSelector]); + +pub fn init(cx: &mut MutableAppContext) { + Picker::::init(cx); + cx.add_action({ + move |workspace, _: &ToggleBaseKeymapSelector, cx| BaseKeymapSelector::toggle(workspace, cx) + }); +} + +pub enum Event { + Dismissed, +} + +impl BaseKeymapSelector { + fn toggle(workspace: &mut Workspace, cx: &mut ViewContext) { + workspace.toggle_modal(cx, |_, cx| { + let this = cx.add_view(|cx| Self::new(cx)); + cx.subscribe(&this, Self::on_event).detach(); + this + }); + } + + fn new(cx: &mut ViewContext) -> Self { + let base = cx.global::().base_keymap; + let selected_index = BaseKeymap::OPTIONS + .iter() + .position(|(_, value)| *value == base) + .unwrap_or(0); + + let this = cx.weak_handle(); + Self { + picker: cx.add_view(|cx| Picker::new("Select a base keymap", this, cx)), + matches: Vec::new(), + selected_index, + } + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } +} + +impl Entity for BaseKeymapSelector { + type Event = Event; +} + +impl View for BaseKeymapSelector { + fn ui_name() -> &'static str { + "BaseKeymapSelector" + } + + fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> gpui::ElementBox { + ChildView::new(self.picker.clone(), cx).boxed() + } + + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.picker); + } + } +} + +impl PickerDelegate for BaseKeymapSelector { + 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) -> gpui::Task<()> { + let background = cx.background().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, cx| { + this.matches = matches; + this.selected_index = this + .selected_index + .min(this.matches.len().saturating_sub(1)); + cx.notify(); + }); + }) + } + + fn confirm(&mut self, cx: &mut ViewContext) { + if let Some(selection) = self.matches.get(self.selected_index) { + let base_keymap = BaseKeymap::from_names(&selection.string); + SettingsFile::update(cx, move |settings| settings.base_keymap = Some(base_keymap)); + } + cx.emit(Event::Dismissed); + } + + fn dismiss(&mut self, cx: &mut ViewContext) { + cx.emit(Event::Dismissed) + } + + fn render_match( + &self, + ix: usize, + mouse_state: &mut gpui::MouseState, + selected: bool, + cx: &gpui::AppContext, + ) -> gpui::ElementBox { + let theme = &cx.global::().theme; + let keymap_match = &self.matches[ix]; + let style = theme.picker.item.style_for(mouse_state, selected); + + Label::new(keymap_match.string.clone(), style.label.clone()) + .with_highlights(keymap_match.positions.clone()) + .contained() + .with_style(style.container) + .boxed() + } +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 8bb055dad0..c780a06ee1 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -1,3 +1,5 @@ +mod base_keymap_picker; + use std::borrow::Cow; use gpui::{ @@ -9,11 +11,15 @@ use settings::{settings_file::SettingsFile, Settings, SettingsFileContent}; use theme::CheckboxStyle; use workspace::{item::Item, PaneBackdrop, Welcome, Workspace, WorkspaceId}; +use crate::base_keymap_picker::ToggleBaseKeymapSelector; + pub fn init(cx: &mut MutableAppContext) { cx.add_action(|workspace: &mut Workspace, _: &Welcome, cx| { let welcome_page = cx.add_view(WelcomePage::new); workspace.add_item(Box::new(welcome_page), cx) - }) + }); + + base_keymap_picker::init(cx); } pub struct WelcomePage { @@ -64,9 +70,9 @@ impl View for WelcomePage { .contained() .with_style(theme.welcome.logo_subheading.container) .boxed(), - self.render_cta_button(2, "Choose a theme", theme_selector::Toggle, width, cx), - self.render_cta_button(3, "Choose a keymap", theme_selector::Toggle, width, cx), - self.render_cta_button(4, "Install the CLI", install_cli::Install, width, cx), + self.render_cta_button("Choose a theme", theme_selector::Toggle, width, cx), + self.render_cta_button("Choose a keymap", ToggleBaseKeymapSelector, width, cx), + self.render_cta_button("Install the CLI", install_cli::Install, width, cx), self.render_settings_checkbox::( "Do you want to send telemetry?", &theme.welcome.checkbox, @@ -110,7 +116,6 @@ impl WelcomePage { fn render_cta_button( &self, - region_id: usize, label: L, action: A, width: f32, @@ -121,7 +126,7 @@ impl WelcomePage { A: 'static + Action + Clone, { let theme = cx.global::().theme.clone(); - MouseEventHandler::::new(region_id, cx, |state, _| { + MouseEventHandler::::new(0, cx, |state, _| { let style = theme.welcome.button.style_for(state, false); Label::new(label, style.text.clone()) .aligned() diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 63e026b9ab..3b48632265 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -35,7 +35,7 @@ use std::{borrow::Cow, env, path::Path, str, sync::Arc}; use util::{channel::ReleaseChannel, paths, ResultExt, StaffMode}; use uuid::Uuid; pub use workspace; -use workspace::{dock::Dock, open_new, sidebar::SidebarSide, AppState, Restart, Workspace}; +use workspace::{open_new, sidebar::SidebarSide, AppState, Restart, Workspace}; pub const FIRST_OPEN: &str = "first_open"; @@ -270,7 +270,6 @@ pub fn init(app_state: &Arc, cx: &mut gpui::MutableAppContext) { workspace.toggle_sidebar(SidebarSide::Left, cx); let welcome_page = cx.add_view(|cx| welcome::WelcomePage::new(cx)); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); - Dock::move_dock(workspace, settings::DockAnchor::Bottom, false, cx); cx.focus(welcome_page); cx.notify(); })