diff --git a/Cargo.lock b/Cargo.lock index a19397bdf9..58e482ee39 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18359,7 +18359,6 @@ dependencies = [ "language", "picker", "project", - "schemars", "serde", "settings", "telemetry", diff --git a/crates/welcome/src/base_keymap_setting.rs b/crates/settings/src/base_keymap_setting.rs similarity index 96% rename from crates/welcome/src/base_keymap_setting.rs rename to crates/settings/src/base_keymap_setting.rs index b841b69f9d..6916d98ae3 100644 --- a/crates/welcome/src/base_keymap_setting.rs +++ b/crates/settings/src/base_keymap_setting.rs @@ -1,8 +1,8 @@ use std::fmt::{Display, Formatter}; +use crate::{Settings, SettingsSources, VsCodeSettings}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; -use settings::{Settings, SettingsSources}; /// Base key bindings scheme. Base keymaps can be overridden with user keymaps. /// @@ -114,7 +114,7 @@ impl Settings for BaseKeymap { sources.default.ok_or_else(Self::missing_default) } - fn import_from_vscode(_vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) { + fn import_from_vscode(_vscode: &VsCodeSettings, current: &mut Self::FileContent) { *current = Some(BaseKeymap::VSCode); } } diff --git a/crates/settings/src/keymap_file.rs b/crates/settings/src/keymap_file.rs index ca54b6a877..b91739ca87 100644 --- a/crates/settings/src/keymap_file.rs +++ b/crates/settings/src/keymap_file.rs @@ -63,7 +63,7 @@ pub struct KeymapSection { /// current file extension are also supported - see [the /// documentation](https://zed.dev/docs/key-bindings#contexts) for more details. #[serde(default)] - context: String, + pub context: String, /// This option enables specifying keys based on their position on a QWERTY keyboard, by using /// position-equivalent mappings for some non-QWERTY keyboards. This is currently only supported /// on macOS. See the documentation for more details. diff --git a/crates/settings/src/settings.rs b/crates/settings/src/settings.rs index f690a2ea93..4e6bd94d92 100644 --- a/crates/settings/src/settings.rs +++ b/crates/settings/src/settings.rs @@ -1,3 +1,4 @@ +mod base_keymap_setting; mod editable_setting_control; mod key_equivalents; mod keymap_file; @@ -11,6 +12,7 @@ use rust_embed::RustEmbed; use std::{borrow::Cow, fmt, str}; use util::asset_str; +pub use base_keymap_setting::*; pub use editable_setting_control::*; pub use key_equivalents::*; pub use keymap_file::{ @@ -71,6 +73,7 @@ pub fn init(cx: &mut App) { .set_default_settings(&default_settings(), cx) .unwrap(); cx.set_global(settings); + BaseKeymap::register(cx); } pub fn default_settings() -> Cow<'static, str> { diff --git a/crates/settings_ui/src/keybindings.rs b/crates/settings_ui/src/keybindings.rs index 34d4b85852..2dd693c798 100644 --- a/crates/settings_ui/src/keybindings.rs +++ b/crates/settings_ui/src/keybindings.rs @@ -5,7 +5,7 @@ use std::{ use anyhow::{Context as _, anyhow}; use collections::HashSet; -use editor::{Editor, EditorEvent}; +use editor::{CompletionProvider, Editor, EditorEvent}; use feature_flags::FeatureFlagViewExt; use fs::Fs; use fuzzy::{StringMatch, StringMatchCandidate}; @@ -14,8 +14,8 @@ use gpui::{ Global, KeyContext, Keystroke, ModifiersChangedEvent, ScrollStrategy, StyledText, Subscription, WeakEntity, actions, div, transparent_black, }; -use language::{Language, LanguageConfig}; -use settings::KeybindSource; +use language::{Language, LanguageConfig, ToOffset as _}; +use settings::{BaseKeymap, KeybindSource, KeymapFile, SettingsAssets}; use util::ResultExt; @@ -850,8 +850,10 @@ impl KeybindingEditorModal { cx: &mut App, ) -> Self { let keybind_editor = cx.new(KeystrokeInput::new); + let context_editor = cx.new(|cx| { let mut editor = Editor::single_line(window, cx); + if let Some(context) = editing_keybind .context .as_ref() @@ -862,6 +864,21 @@ impl KeybindingEditorModal { editor.set_placeholder_text("Keybinding context", cx); } + cx.spawn(async |editor, cx| { + let contexts = cx + .background_spawn(async { collect_contexts_from_assets() }) + .await; + + editor + .update(cx, |editor, _cx| { + editor.set_completion_provider(Some(std::rc::Rc::new( + KeyContextCompletionProvider { contexts }, + ))); + }) + .context("Failed to load completions for keybinding context") + }) + .detach_and_log_err(cx); + editor }); Self { @@ -1001,6 +1018,69 @@ impl Render for KeybindingEditorModal { } } +struct KeyContextCompletionProvider { + contexts: Vec, +} + +impl CompletionProvider for KeyContextCompletionProvider { + fn completions( + &self, + _excerpt_id: editor::ExcerptId, + buffer: &Entity, + buffer_position: language::Anchor, + _trigger: editor::CompletionContext, + _window: &mut Window, + cx: &mut Context, + ) -> gpui::Task>> { + let buffer = buffer.read(cx); + let mut count_back = 0; + for char in buffer.reversed_chars_at(buffer_position) { + if char.is_ascii_alphanumeric() || char == '_' { + count_back += 1; + } else { + break; + } + } + let start_anchor = buffer.anchor_before( + buffer_position + .to_offset(&buffer) + .saturating_sub(count_back), + ); + let replace_range = start_anchor..buffer_position; + gpui::Task::ready(Ok(vec![project::CompletionResponse { + completions: self + .contexts + .iter() + .map(|context| project::Completion { + replace_range: replace_range.clone(), + label: language::CodeLabel::plain(context.to_string(), None), + new_text: context.to_string(), + documentation: None, + source: project::CompletionSource::Custom, + icon_path: None, + insert_text_mode: None, + confirm: None, + }) + .collect(), + is_incomplete: false, + }])) + } + + fn is_completion_trigger( + &self, + _buffer: &Entity, + _position: language::Anchor, + text: &str, + _trigger_in_words: bool, + _menu_is_open: bool, + _cx: &mut Context, + ) -> bool { + text.chars().last().map_or(false, |last_char| { + last_char.is_ascii_alphanumeric() || last_char == '_' + }) + } +} + async fn save_keybinding_update( existing: ProcessedKeybinding, new_keystrokes: &[Keystroke], @@ -1254,6 +1334,72 @@ fn build_keybind_context_menu( }) } +fn collect_contexts_from_assets() -> Vec { + let mut keymap_assets = vec![ + util::asset_str::(settings::DEFAULT_KEYMAP_PATH), + util::asset_str::(settings::VIM_KEYMAP_PATH), + ]; + keymap_assets.extend( + BaseKeymap::OPTIONS + .iter() + .filter_map(|(_, base_keymap)| base_keymap.asset_path()) + .map(util::asset_str::), + ); + + let mut contexts = HashSet::default(); + + for keymap_asset in keymap_assets { + let Ok(keymap) = KeymapFile::parse(&keymap_asset) else { + continue; + }; + + for section in keymap.sections() { + let context_expr = §ion.context; + let mut queue = Vec::new(); + let Ok(root_context) = gpui::KeyBindingContextPredicate::parse(context_expr) else { + continue; + }; + + queue.push(root_context); + while let Some(context) = queue.pop() { + match context { + gpui::KeyBindingContextPredicate::Identifier(ident) => { + contexts.insert(ident); + } + gpui::KeyBindingContextPredicate::Equal(ident_a, ident_b) => { + contexts.insert(ident_a); + contexts.insert(ident_b); + } + gpui::KeyBindingContextPredicate::NotEqual(ident_a, ident_b) => { + contexts.insert(ident_a); + contexts.insert(ident_b); + } + gpui::KeyBindingContextPredicate::Child(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + gpui::KeyBindingContextPredicate::Not(ctx) => { + queue.push(*ctx); + } + gpui::KeyBindingContextPredicate::And(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + gpui::KeyBindingContextPredicate::Or(ctx_a, ctx_b) => { + queue.push(*ctx_a); + queue.push(*ctx_b); + } + } + } + } + } + + let mut contexts = contexts.into_iter().collect::>(); + contexts.sort(); + + return contexts; +} + impl SerializableItem for KeymapEditor { fn serialized_item_kind() -> &'static str { "KeymapEditor" diff --git a/crates/welcome/Cargo.toml b/crates/welcome/Cargo.toml index 6d4896016c..769dd8d6aa 100644 --- a/crates/welcome/Cargo.toml +++ b/crates/welcome/Cargo.toml @@ -26,7 +26,6 @@ install_cli.workspace = true language.workspace = true picker.workspace = true project.workspace = true -schemars.workspace = true serde.workspace = true settings.workspace = true telemetry.workspace = true diff --git a/crates/welcome/src/base_keymap_picker.rs b/crates/welcome/src/base_keymap_picker.rs index d5a6ae96da..92317ca711 100644 --- a/crates/welcome/src/base_keymap_picker.rs +++ b/crates/welcome/src/base_keymap_picker.rs @@ -1,4 +1,3 @@ -use super::base_keymap_setting::BaseKeymap; use fuzzy::{StringMatch, StringMatchCandidate, match_strings}; use gpui::{ App, Context, DismissEvent, Entity, EventEmitter, Focusable, Render, Task, WeakEntity, Window, @@ -6,7 +5,7 @@ use gpui::{ }; use picker::{Picker, PickerDelegate}; use project::Fs; -use settings::{Settings, update_settings_file}; +use settings::{BaseKeymap, Settings, update_settings_file}; use std::sync::Arc; use ui::{ListItem, ListItemSpacing, prelude::*}; use util::ResultExt; diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 74d7323d8c..ea4ac13de7 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -17,11 +17,9 @@ use workspace::{ open_new, }; -pub use base_keymap_setting::BaseKeymap; pub use multibuffer_hint::*; mod base_keymap_picker; -mod base_keymap_setting; mod multibuffer_hint; mod welcome_ui; @@ -37,8 +35,6 @@ pub const FIRST_OPEN: &str = "first_open"; pub const DOCS_URL: &str = "https://zed.dev/docs/"; pub fn init(cx: &mut App) { - BaseKeymap::register(cx); - cx.observe_new(|workspace: &mut Workspace, _, _cx| { workspace.register_action(|workspace, _: &Welcome, window, cx| { let welcome_page = WelcomePage::new(workspace, cx); diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index e04e9c38c1..3c46c486a8 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -29,7 +29,7 @@ use project::project_settings::ProjectSettings; use recent_projects::{SshSettings, open_ssh_project}; use release_channel::{AppCommitSha, AppVersion, ReleaseChannel}; use session::{AppSession, Session}; -use settings::{Settings, SettingsStore, watch_config_file}; +use settings::{BaseKeymap, Settings, SettingsStore, watch_config_file}; use std::{ env, io::{self, IsTerminal}, @@ -43,7 +43,7 @@ use theme::{ }; use util::{ConnectionResult, ResultExt, TryFutureExt, maybe}; use uuid::Uuid; -use welcome::{BaseKeymap, FIRST_OPEN, show_welcome_view}; +use welcome::{FIRST_OPEN, show_welcome_view}; use workspace::{ AppState, SerializedWorkspaceLocation, Toast, Workspace, WorkspaceSettings, WorkspaceStore, notifications::NotificationId, diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 10fdcf34a6..dc094a6c12 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -48,9 +48,10 @@ use release_channel::{AppCommitSha, ReleaseChannel}; use rope::Rope; use search::project_search::ProjectSearchBar; use settings::{ - DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, KeymapFileLoadResult, - Settings, SettingsStore, VIM_KEYMAP_PATH, initial_local_debug_tasks_content, - initial_project_settings_content, initial_tasks_content, update_settings_file, + BaseKeymap, DEFAULT_KEYMAP_PATH, InvalidSettingsError, KeybindSource, KeymapFile, + KeymapFileLoadResult, Settings, SettingsStore, VIM_KEYMAP_PATH, + initial_local_debug_tasks_content, initial_project_settings_content, initial_tasks_content, + update_settings_file, }; use std::path::PathBuf; use std::sync::atomic::{self, AtomicBool}; @@ -62,7 +63,7 @@ use util::markdown::MarkdownString; use util::{ResultExt, asset_str}; use uuid::Uuid; use vim_mode_setting::VimModeSetting; -use welcome::{BaseKeymap, DOCS_URL, MultibufferHint}; +use welcome::{DOCS_URL, MultibufferHint}; use workspace::notifications::{NotificationId, dismiss_app_notification, show_app_notification}; use workspace::{ AppState, NewFile, NewWindow, OpenLog, Toast, Workspace, WorkspaceSettings,