keymap_ui: Add auto-complete for context in keybind editor (#34031)
Closes #ISSUE Implements a very basic completion provider that is attached to the context editor in the keybind editing modal. The context identifiers used for completions are scraped from the default, vim, and base keymaps on demand. Release Notes: - N/A *or* Added/Fixed/Improved ...
This commit is contained in:
parent
66a1c356bf
commit
877ef5e1b1
10 changed files with 163 additions and 20 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -18359,7 +18359,6 @@ dependencies = [
|
|||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"telemetry",
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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<SharedString>,
|
||||
}
|
||||
|
||||
impl CompletionProvider for KeyContextCompletionProvider {
|
||||
fn completions(
|
||||
&self,
|
||||
_excerpt_id: editor::ExcerptId,
|
||||
buffer: &Entity<language::Buffer>,
|
||||
buffer_position: language::Anchor,
|
||||
_trigger: editor::CompletionContext,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<anyhow::Result<Vec<project::CompletionResponse>>> {
|
||||
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<language::Buffer>,
|
||||
_position: language::Anchor,
|
||||
text: &str,
|
||||
_trigger_in_words: bool,
|
||||
_menu_is_open: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> 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<SharedString> {
|
||||
let mut keymap_assets = vec![
|
||||
util::asset_str::<SettingsAssets>(settings::DEFAULT_KEYMAP_PATH),
|
||||
util::asset_str::<SettingsAssets>(settings::VIM_KEYMAP_PATH),
|
||||
];
|
||||
keymap_assets.extend(
|
||||
BaseKeymap::OPTIONS
|
||||
.iter()
|
||||
.filter_map(|(_, base_keymap)| base_keymap.asset_path())
|
||||
.map(util::asset_str::<SettingsAssets>),
|
||||
);
|
||||
|
||||
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::<Vec<_>>();
|
||||
contexts.sort();
|
||||
|
||||
return contexts;
|
||||
}
|
||||
|
||||
impl SerializableItem for KeymapEditor {
|
||||
fn serialized_item_kind() -> &'static str {
|
||||
"KeymapEditor"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue