mirror of
https://github.com/helix-editor/helix.git
synced 2025-10-06 00:13:28 +02:00
Compare commits
5 Commits
d0218f7e78
...
md-composi
Author | SHA1 | Date | |
---|---|---|---|
|
136704b3fb | ||
|
71b6cc4d17 | ||
|
5ca3ed3ef8 | ||
|
21169c77fd | ||
|
d6fc6a54b2 |
@@ -63,6 +63,7 @@ type Terminal = tui::terminal::Terminal<TerminalBackend>;
|
||||
|
||||
pub struct Application {
|
||||
compositor: Compositor,
|
||||
keymaps: Keymaps,
|
||||
terminal: Terminal,
|
||||
pub editor: Editor,
|
||||
|
||||
@@ -156,7 +157,8 @@ impl Application {
|
||||
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
|
||||
&config.keys
|
||||
}));
|
||||
let editor_view = Box::new(ui::EditorView::new(Keymaps::new(keys)));
|
||||
let keymaps = Keymaps::new(keys);
|
||||
let editor_view = Box::new(ui::EditorView::default());
|
||||
compositor.push(editor_view);
|
||||
|
||||
if args.load_tutor {
|
||||
@@ -241,6 +243,7 @@ impl Application {
|
||||
.context("build signal handler")?;
|
||||
|
||||
let app = Self {
|
||||
keymaps,
|
||||
compositor,
|
||||
terminal,
|
||||
editor,
|
||||
@@ -261,6 +264,7 @@ impl Application {
|
||||
|
||||
async fn render(&mut self) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
keymaps: &mut self.keymaps,
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
scroll: None,
|
||||
@@ -521,6 +525,7 @@ impl Application {
|
||||
|
||||
pub async fn handle_idle_timeout(&mut self) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
keymaps: &mut self.keymaps,
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
scroll: None,
|
||||
@@ -641,6 +646,7 @@ impl Application {
|
||||
event: Result<CrosstermEvent, crossterm::ErrorKind>,
|
||||
) {
|
||||
let mut cx = crate::compositor::Context {
|
||||
keymaps: &mut self.keymaps,
|
||||
editor: &mut self.editor,
|
||||
jobs: &mut self.jobs,
|
||||
scroll: None,
|
||||
|
@@ -50,10 +50,10 @@ use movement::Movement;
|
||||
|
||||
use crate::{
|
||||
args,
|
||||
compositor::{self, Component, Compositor},
|
||||
compositor::{self, Component, Compositor, EventResult},
|
||||
filter_picker_entry,
|
||||
job::Callback,
|
||||
keymap::ReverseKeymap,
|
||||
keymap::{Keymaps, ReverseKeymap},
|
||||
ui::{
|
||||
self, editor::InsertEvent, lsp::SignatureHelp, overlay::overlaid, CompletionItem, Picker,
|
||||
Popup, Prompt, PromptEvent,
|
||||
@@ -81,6 +81,8 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
pub type OnKeyCallback = Box<dyn FnOnce(&mut Context, KeyEvent)>;
|
||||
|
||||
pub struct Context<'a> {
|
||||
pub keymaps: &'a mut Keymaps,
|
||||
|
||||
pub register: Option<char>,
|
||||
pub count: Option<NonZeroUsize>,
|
||||
pub editor: &'a mut Editor,
|
||||
@@ -170,6 +172,11 @@ pub enum MappableCommand {
|
||||
fun: fn(cx: &mut Context),
|
||||
doc: &'static str,
|
||||
},
|
||||
Component {
|
||||
name: &'static str,
|
||||
fun: fn(&mut dyn crate::compositor::Component, &mut compositor::Context) -> EventResult,
|
||||
doc: &'static str,
|
||||
},
|
||||
}
|
||||
|
||||
macro_rules! static_commands {
|
||||
@@ -196,6 +203,7 @@ impl MappableCommand {
|
||||
let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
|
||||
if let Some(command) = typed::TYPABLE_COMMAND_MAP.get(name.as_str()) {
|
||||
let mut cx = compositor::Context {
|
||||
keymaps: cx.keymaps,
|
||||
editor: cx.editor,
|
||||
jobs: cx.jobs,
|
||||
scroll: None,
|
||||
@@ -206,6 +214,7 @@ impl MappableCommand {
|
||||
}
|
||||
}
|
||||
Self::Static { fun, .. } => (fun)(cx),
|
||||
Self::Component { .. } => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,6 +222,7 @@ impl MappableCommand {
|
||||
match &self {
|
||||
Self::Typable { name, .. } => name,
|
||||
Self::Static { name, .. } => name,
|
||||
Self::Component { .. } => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,9 +230,18 @@ impl MappableCommand {
|
||||
match &self {
|
||||
Self::Typable { doc, .. } => doc,
|
||||
Self::Static { doc, .. } => doc,
|
||||
Self::Component { .. } => unimplemented!(),
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: macro for this...
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const close_buffer_in_buffer_picker: Self = Self::Component {
|
||||
name: "close_buffer_in_buffer_picker",
|
||||
fun: crate::ui::picker::close_buffer_in_buffer_picker,
|
||||
doc: "Closes the currently focused buffer",
|
||||
};
|
||||
|
||||
#[rustfmt::skip]
|
||||
static_commands!(
|
||||
no_op, "Do nothing",
|
||||
@@ -500,6 +519,7 @@ impl fmt::Debug for MappableCommand {
|
||||
.field(name)
|
||||
.field(args)
|
||||
.finish(),
|
||||
Self::Component { .. } => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2523,17 +2543,19 @@ fn file_picker_in_current_directory(cx: &mut Context) {
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
pub struct BufferMeta {
|
||||
pub id: DocumentId,
|
||||
path: Option<PathBuf>,
|
||||
is_modified: bool,
|
||||
is_current: bool,
|
||||
focused_at: std::time::Instant,
|
||||
}
|
||||
|
||||
pub type BufferPicker = Picker<BufferMeta>;
|
||||
|
||||
fn buffer_picker(cx: &mut Context) {
|
||||
let current = view!(cx.editor).doc;
|
||||
|
||||
struct BufferMeta {
|
||||
id: DocumentId,
|
||||
path: Option<PathBuf>,
|
||||
is_modified: bool,
|
||||
is_current: bool,
|
||||
focused_at: std::time::Instant,
|
||||
}
|
||||
|
||||
impl ui::menu::Item for BufferMeta {
|
||||
type Data = ();
|
||||
|
||||
@@ -2588,7 +2610,8 @@ fn buffer_picker(cx: &mut Context) {
|
||||
.primary()
|
||||
.cursor_line(doc.text().slice(..));
|
||||
Some((meta.id.into(), Some((line, line))))
|
||||
});
|
||||
})
|
||||
.with_id("buffer-picker");
|
||||
cx.push_layer(Box::new(overlaid(picker)));
|
||||
}
|
||||
|
||||
@@ -2706,6 +2729,7 @@ impl ui::menu::Item for MappableCommand {
|
||||
Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(),
|
||||
None => format!("{} [{}]", doc, name).into(),
|
||||
},
|
||||
MappableCommand::Component { .. } => unimplemented!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2716,9 +2740,8 @@ pub fn command_palette(cx: &mut Context) {
|
||||
|
||||
cx.callback = Some(Box::new(
|
||||
move |compositor: &mut Compositor, cx: &mut compositor::Context| {
|
||||
let keymap = compositor.find::<ui::EditorView>().unwrap().keymaps.map()
|
||||
[&cx.editor.mode]
|
||||
.reverse_map();
|
||||
let keymap =
|
||||
cx.keymaps.map()[&crate::keymap::Domain::Mode(cx.editor.mode)].reverse_map();
|
||||
|
||||
let mut commands: Vec<MappableCommand> = MappableCommand::STATIC_COMMAND_LIST.into();
|
||||
commands.extend(typed::TYPABLE_COMMAND_LIST.iter().map(|cmd| {
|
||||
@@ -2733,6 +2756,7 @@ pub fn command_palette(cx: &mut Context) {
|
||||
let mut ctx = Context {
|
||||
register,
|
||||
count,
|
||||
keymaps: cx.keymaps,
|
||||
editor: cx.editor,
|
||||
callback: None,
|
||||
on_next_key_callback: None,
|
||||
|
@@ -15,12 +15,13 @@ pub enum EventResult {
|
||||
Consumed(Option<Callback>),
|
||||
}
|
||||
|
||||
use crate::job::Jobs;
|
||||
use crate::{job::Jobs, keymap::Keymaps};
|
||||
use helix_view::Editor;
|
||||
|
||||
pub use helix_view::input::Event;
|
||||
|
||||
pub struct Context<'a> {
|
||||
pub keymaps: &'a mut Keymaps,
|
||||
pub editor: &'a mut Editor,
|
||||
pub scroll: Option<usize>,
|
||||
pub jobs: &'a mut Jobs,
|
||||
|
@@ -1,7 +1,6 @@
|
||||
use crate::keymap;
|
||||
use crate::keymap::{merge_keys, KeyTrie};
|
||||
use crate::keymap::{merge_keys, Domain, KeyTrie};
|
||||
use helix_loader::merge_toml_values;
|
||||
use helix_view::document::Mode;
|
||||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::fmt::Display;
|
||||
@@ -12,7 +11,7 @@ use toml::de::Error as TomlError;
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Config {
|
||||
pub theme: Option<String>,
|
||||
pub keys: HashMap<Mode, KeyTrie>,
|
||||
pub keys: HashMap<Domain, KeyTrie>,
|
||||
pub editor: helix_view::editor::Config,
|
||||
}
|
||||
|
||||
@@ -20,7 +19,7 @@ pub struct Config {
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ConfigRaw {
|
||||
pub theme: Option<String>,
|
||||
pub keys: Option<HashMap<Mode, KeyTrie>>,
|
||||
pub keys: Option<HashMap<Domain, KeyTrie>>,
|
||||
pub editor: Option<toml::Value>,
|
||||
}
|
||||
|
||||
@@ -154,11 +153,11 @@ mod tests {
|
||||
merge_keys(
|
||||
&mut keys,
|
||||
hashmap! {
|
||||
Mode::Insert => keymap!({ "Insert mode"
|
||||
Domain::Mode(Mode::Insert) => keymap!({ "Insert mode"
|
||||
"y" => move_line_down,
|
||||
"S-C-a" => delete_selection,
|
||||
}),
|
||||
Mode::Normal => keymap!({ "Normal mode"
|
||||
Domain::Mode(Mode::Normal) => keymap!({ "Normal mode"
|
||||
"A-F12" => move_next_word_end,
|
||||
}),
|
||||
},
|
||||
|
@@ -272,8 +272,43 @@ pub enum KeymapResult {
|
||||
/// A map of command names to keybinds that will execute the command.
|
||||
pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
|
||||
|
||||
// TODO name
|
||||
#[derive(Eq, Hash, PartialEq, Clone, Debug)]
|
||||
pub enum Domain {
|
||||
Mode(Mode),
|
||||
Component(&'static str),
|
||||
}
|
||||
|
||||
const REMAPPABLE_COMPONENTS: [&'static str; 3] = [
|
||||
crate::ui::DYNAMIC_PICKER_ID,
|
||||
crate::ui::PICKER_ID,
|
||||
// TODO: make it a constant
|
||||
"buffer-picker",
|
||||
];
|
||||
|
||||
impl<'de> Deserialize<'de> for Domain {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let s = String::deserialize(deserializer)?;
|
||||
if let Ok(mode) = s.parse::<Mode>() {
|
||||
return Ok(Domain::Mode(mode));
|
||||
} else if let Some(name) = REMAPPABLE_COMPONENTS
|
||||
.iter()
|
||||
.find(|name| **name == s.as_str())
|
||||
{
|
||||
Ok(Domain::Component(name))
|
||||
} else {
|
||||
Err(serde::de::Error::custom(format!(
|
||||
"Unknown keymap domain {s}. Expected a mode or component name"
|
||||
)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Keymaps {
|
||||
pub map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>,
|
||||
pub map: Box<dyn DynAccess<HashMap<Domain, KeyTrie>>>,
|
||||
/// Stores pending keys waiting for the next key. This is relative to a
|
||||
/// sticky node if one is in use.
|
||||
state: Vec<KeyEvent>,
|
||||
@@ -282,7 +317,7 @@ pub struct Keymaps {
|
||||
}
|
||||
|
||||
impl Keymaps {
|
||||
pub fn new(map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>) -> Self {
|
||||
pub fn new(map: Box<dyn DynAccess<HashMap<Domain, KeyTrie>>>) -> Self {
|
||||
Self {
|
||||
map,
|
||||
state: Vec::new(),
|
||||
@@ -290,7 +325,7 @@ impl Keymaps {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map(&self) -> DynGuard<HashMap<Mode, KeyTrie>> {
|
||||
pub fn map(&self) -> DynGuard<HashMap<Domain, KeyTrie>> {
|
||||
self.map.load()
|
||||
}
|
||||
|
||||
@@ -303,14 +338,24 @@ impl Keymaps {
|
||||
self.sticky.as_ref()
|
||||
}
|
||||
|
||||
pub fn get_by_mode(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
|
||||
self.get(Domain::Mode(mode), key)
|
||||
}
|
||||
|
||||
pub fn get_by_component_id(&mut self, id: &'static str, key: KeyEvent) -> KeymapResult {
|
||||
self.get(Domain::Component(id), key)
|
||||
}
|
||||
|
||||
/// Lookup `key` in the keymap to try and find a command to execute. Escape
|
||||
/// key cancels pending keystrokes. If there are no pending keystrokes but a
|
||||
/// sticky node is in use, it will be cleared.
|
||||
pub fn get(&mut self, mode: Mode, key: KeyEvent) -> KeymapResult {
|
||||
// TODO: remove the sticky part and look up manually
|
||||
fn get(&mut self, domain: Domain, key: KeyEvent) -> KeymapResult {
|
||||
let keymaps = &*self.map();
|
||||
let keymap = &keymaps[&mode];
|
||||
let Some(keymap) = keymaps.get(&domain) else {
|
||||
return KeymapResult::NotFound;
|
||||
};
|
||||
|
||||
// TODO: remove the sticky part and look up manually
|
||||
if key!(Esc) == key {
|
||||
if !self.state.is_empty() {
|
||||
// Note that Esc is not included here
|
||||
@@ -365,7 +410,7 @@ impl Default for Keymaps {
|
||||
}
|
||||
|
||||
/// Merge default config keys with user overwritten keys for custom user config.
|
||||
pub fn merge_keys(dst: &mut HashMap<Mode, KeyTrie>, mut delta: HashMap<Mode, KeyTrie>) {
|
||||
pub fn merge_keys(dst: &mut HashMap<Domain, KeyTrie>, mut delta: HashMap<Domain, KeyTrie>) {
|
||||
for (mode, keys) in dst {
|
||||
keys.merge_nodes(
|
||||
delta
|
||||
@@ -400,7 +445,7 @@ mod tests {
|
||||
#[test]
|
||||
fn merge_partial_keys() {
|
||||
let keymap = hashmap! {
|
||||
Mode::Normal => keymap!({ "Normal mode"
|
||||
Domain::Mode(Mode::Normal) => keymap!({ "Normal mode"
|
||||
"i" => normal_mode,
|
||||
"无" => insert_mode,
|
||||
"z" => jump_backward,
|
||||
@@ -416,23 +461,23 @@ mod tests {
|
||||
|
||||
let mut keymap = Keymaps::new(Box::new(Constant(merged_keyamp.clone())));
|
||||
assert_eq!(
|
||||
keymap.get(Mode::Normal, key!('i')),
|
||||
keymap.get_by_mode(Mode::Normal, key!('i')),
|
||||
KeymapResult::Matched(MappableCommand::normal_mode),
|
||||
"Leaf should replace leaf"
|
||||
);
|
||||
assert_eq!(
|
||||
keymap.get(Mode::Normal, key!('无')),
|
||||
keymap.get_by_mode(Mode::Normal, key!('无')),
|
||||
KeymapResult::Matched(MappableCommand::insert_mode),
|
||||
"New leaf should be present in merged keymap"
|
||||
);
|
||||
// Assumes that z is a node in the default keymap
|
||||
assert_eq!(
|
||||
keymap.get(Mode::Normal, key!('z')),
|
||||
keymap.get_by_mode(Mode::Normal, key!('z')),
|
||||
KeymapResult::Matched(MappableCommand::jump_backward),
|
||||
"Leaf should replace node"
|
||||
);
|
||||
|
||||
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
|
||||
let keymap = merged_keyamp.get_mut(&Domain::Mode(Mode::Normal)).unwrap();
|
||||
// Assumes that `g` is a node in default keymap
|
||||
assert_eq!(
|
||||
keymap.search(&[key!('g'), key!('$')]).unwrap(),
|
||||
@@ -454,7 +499,7 @@ mod tests {
|
||||
|
||||
assert!(
|
||||
merged_keyamp
|
||||
.get(&Mode::Normal)
|
||||
.get(&Domain::Mode(Mode::Normal))
|
||||
.and_then(|key_trie| key_trie.node())
|
||||
.unwrap()
|
||||
.len()
|
||||
@@ -462,7 +507,7 @@ mod tests {
|
||||
);
|
||||
assert!(
|
||||
merged_keyamp
|
||||
.get(&Mode::Insert)
|
||||
.get(&Domain::Mode(Mode::Insert))
|
||||
.and_then(|key_trie| key_trie.node())
|
||||
.unwrap()
|
||||
.len()
|
||||
@@ -473,7 +518,7 @@ mod tests {
|
||||
#[test]
|
||||
fn order_should_be_set() {
|
||||
let keymap = hashmap! {
|
||||
Mode::Normal => keymap!({ "Normal mode"
|
||||
Domain::Mode(Mode::Normal) => keymap!({ "Normal mode"
|
||||
"space" => { ""
|
||||
"s" => { ""
|
||||
"v" => vsplit,
|
||||
@@ -485,7 +530,7 @@ mod tests {
|
||||
let mut merged_keyamp = default();
|
||||
merge_keys(&mut merged_keyamp, keymap.clone());
|
||||
assert_ne!(keymap, merged_keyamp);
|
||||
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
|
||||
let keymap = merged_keyamp.get_mut(&Domain::Mode(Mode::Normal)).unwrap();
|
||||
// Make sure mapping works
|
||||
assert_eq!(
|
||||
keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(),
|
||||
@@ -500,7 +545,7 @@ mod tests {
|
||||
#[test]
|
||||
fn aliased_modes_are_same_in_default_keymap() {
|
||||
let keymaps = Keymaps::default().map();
|
||||
let root = keymaps.get(&Mode::Normal).unwrap();
|
||||
let root = keymaps.get(&Domain::Mode(Mode::Normal)).unwrap();
|
||||
assert_eq!(
|
||||
root.search(&[key!(' '), key!('w')]).unwrap(),
|
||||
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
|
||||
|
@@ -1,10 +1,10 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::macros::keymap;
|
||||
use super::{KeyTrie, Mode};
|
||||
use super::{Domain, KeyTrie, Mode};
|
||||
use helix_core::hashmap;
|
||||
|
||||
pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
pub fn default() -> HashMap<Domain, KeyTrie> {
|
||||
let normal = keymap!({ "Normal mode"
|
||||
"h" | "left" => move_char_left,
|
||||
"j" | "down" => move_visual_line_down,
|
||||
@@ -379,9 +379,15 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
|
||||
"home" => goto_line_start,
|
||||
"end" => goto_line_end_newline,
|
||||
});
|
||||
|
||||
let buffer_picker = keymap!({ "Buffer picker"
|
||||
"C-x" => close_buffer_in_buffer_picker,
|
||||
});
|
||||
|
||||
hashmap!(
|
||||
Mode::Normal => normal,
|
||||
Mode::Select => select,
|
||||
Mode::Insert => insert,
|
||||
Domain::Mode(Mode::Normal) => normal,
|
||||
Domain::Mode(Mode::Select) => select,
|
||||
Domain::Mode(Mode::Insert) => insert,
|
||||
Domain::Component("buffer-picker") => buffer_picker,
|
||||
)
|
||||
}
|
||||
|
@@ -3,7 +3,7 @@ use crate::{
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
job::{self, Callback},
|
||||
key,
|
||||
keymap::{KeymapResult, Keymaps},
|
||||
keymap::KeymapResult,
|
||||
ui::{
|
||||
document::{render_document, LinePos, TextRenderer, TranslatedPosition},
|
||||
Completion, ProgressSpinners,
|
||||
@@ -37,7 +37,6 @@ use super::{completion::CompletionItem, statusline};
|
||||
use super::{document::LineDecoration, lsp::SignatureHelp};
|
||||
|
||||
pub struct EditorView {
|
||||
pub keymaps: Keymaps,
|
||||
on_next_key: Option<OnKeyCallback>,
|
||||
pseudo_pending: Vec<KeyEvent>,
|
||||
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
|
||||
@@ -58,14 +57,7 @@ pub enum InsertEvent {
|
||||
|
||||
impl Default for EditorView {
|
||||
fn default() -> Self {
|
||||
Self::new(Keymaps::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn new(keymaps: Keymaps) -> Self {
|
||||
Self {
|
||||
keymaps,
|
||||
on_next_key: None,
|
||||
pseudo_pending: Vec::new(),
|
||||
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
|
||||
@@ -73,7 +65,9 @@ impl EditorView {
|
||||
spinners: ProgressSpinners::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
|
||||
&mut self.spinners
|
||||
}
|
||||
@@ -786,7 +780,7 @@ impl EditorView {
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle events by looking them up in `self.keymaps`. Returns None
|
||||
/// Handle events by looking them up in `cxt.keymaps`. Returns None
|
||||
/// if event was handled (a command was executed or a subkeymap was
|
||||
/// activated). Only KeymapResult::{NotFound, Cancelled} is returned
|
||||
/// otherwise.
|
||||
@@ -797,9 +791,9 @@ impl EditorView {
|
||||
event: KeyEvent,
|
||||
) -> Option<KeymapResult> {
|
||||
let mut last_mode = mode;
|
||||
self.pseudo_pending.extend(self.keymaps.pending());
|
||||
let key_result = self.keymaps.get(mode, event);
|
||||
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
|
||||
self.pseudo_pending.extend(cxt.keymaps.pending());
|
||||
let key_result = cxt.keymaps.get_by_mode(mode, event);
|
||||
cxt.editor.autoinfo = cxt.keymaps.sticky().map(|node| node.infobox());
|
||||
|
||||
let mut execute_command = |command: &commands::MappableCommand| {
|
||||
command.execute(cxt);
|
||||
@@ -864,7 +858,7 @@ impl EditorView {
|
||||
Some(ch) => commands::insert::insert_char(cx, ch),
|
||||
None => {
|
||||
if let KeymapResult::Matched(command) =
|
||||
self.keymaps.get(Mode::Insert, ev)
|
||||
cx.keymaps.get_by_mode(Mode::Insert, ev)
|
||||
{
|
||||
command.execute(cx);
|
||||
}
|
||||
@@ -886,7 +880,7 @@ impl EditorView {
|
||||
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
|
||||
}
|
||||
// special handling for repeat operator
|
||||
(key!('.'), _) if self.keymaps.pending().is_empty() => {
|
||||
(key!('.'), _) if cxt.keymaps.pending().is_empty() => {
|
||||
for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) {
|
||||
// first execute whatever put us into insert mode
|
||||
self.last_insert.0.execute(cxt);
|
||||
@@ -944,7 +938,7 @@ impl EditorView {
|
||||
cxt.register = cxt.editor.selected_register.take();
|
||||
|
||||
self.handle_keymap_event(mode, cxt, event);
|
||||
if self.keymaps.pending().is_empty() {
|
||||
if cxt.keymaps.pending().is_empty() {
|
||||
cxt.editor.count = None
|
||||
} else {
|
||||
cxt.editor.selected_register = cxt.register.take();
|
||||
@@ -1225,6 +1219,7 @@ impl Component for EditorView {
|
||||
context: &mut crate::compositor::Context,
|
||||
) -> EventResult {
|
||||
let mut cx = commands::Context {
|
||||
keymaps: context.keymaps,
|
||||
editor: context.editor,
|
||||
count: None,
|
||||
register: None,
|
||||
@@ -1280,6 +1275,7 @@ impl Component for EditorView {
|
||||
let res = {
|
||||
// use a fake context here
|
||||
let mut cx = Context {
|
||||
keymaps: cx.keymaps,
|
||||
editor: cx.editor,
|
||||
jobs: cx.jobs,
|
||||
scroll: None,
|
||||
@@ -1445,7 +1441,7 @@ impl Component for EditorView {
|
||||
if let Some(count) = cx.editor.count {
|
||||
disp.push_str(&count.to_string())
|
||||
}
|
||||
for key in self.keymaps.pending() {
|
||||
for key in cx.keymaps.pending() {
|
||||
disp.push_str(&key.key_sequence_format());
|
||||
}
|
||||
for key in &self.pseudo_pending {
|
||||
|
@@ -7,7 +7,7 @@ pub mod lsp;
|
||||
mod markdown;
|
||||
pub mod menu;
|
||||
pub mod overlay;
|
||||
mod picker;
|
||||
pub mod picker;
|
||||
pub mod popup;
|
||||
mod prompt;
|
||||
mod spinner;
|
||||
@@ -21,7 +21,7 @@ pub use completion::{Completion, CompletionItem};
|
||||
pub use editor::EditorView;
|
||||
pub use markdown::Markdown;
|
||||
pub use menu::Menu;
|
||||
pub use picker::{DynamicPicker, FileLocation, Picker};
|
||||
pub use picker::{DynamicPicker, FileLocation, Picker, DYNAMIC_PICKER_ID, PICKER_ID};
|
||||
pub use popup::Popup;
|
||||
pub use prompt::{Prompt, PromptEvent};
|
||||
pub use spinner::{ProgressSpinners, Spinner};
|
||||
|
@@ -114,6 +114,8 @@ impl Preview<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub const PICKER_ID: &'static str = "picker";
|
||||
|
||||
pub struct Picker<T: Item> {
|
||||
options: Vec<T>,
|
||||
editor_data: T::Data,
|
||||
@@ -141,6 +143,9 @@ pub struct Picker<T: Item> {
|
||||
read_buffer: Vec<u8>,
|
||||
/// Given an item in the picker, return the file path and line number to display.
|
||||
file_fn: Option<FileCallback<T>>,
|
||||
|
||||
/// A unique identifier for the picker as a Component
|
||||
id: &'static str,
|
||||
}
|
||||
|
||||
impl<T: Item + 'static> Picker<T> {
|
||||
@@ -172,6 +177,7 @@ impl<T: Item + 'static> Picker<T> {
|
||||
preview_cache: HashMap::new(),
|
||||
read_buffer: Vec::with_capacity(1024),
|
||||
file_fn: None,
|
||||
id: PICKER_ID,
|
||||
};
|
||||
|
||||
picker.calculate_column_widths();
|
||||
@@ -205,6 +211,11 @@ impl<T: Item + 'static> Picker<T> {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_id(mut self, id: &'static str) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_options(&mut self, new_options: Vec<T>) {
|
||||
self.options = new_options;
|
||||
self.cursor = 0;
|
||||
@@ -791,6 +802,18 @@ impl<T: Item + 'static> Component for Picker<T> {
|
||||
_ => return EventResult::Ignored(None),
|
||||
};
|
||||
|
||||
match ctx.keymaps.get_by_component_id(self.id, key_event) {
|
||||
crate::keymap::KeymapResult::Matched(crate::keymap::MappableCommand::Component {
|
||||
fun,
|
||||
..
|
||||
}) => {
|
||||
if let EventResult::Consumed(callback) = fun(self, ctx) {
|
||||
return EventResult::Consumed(callback);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let close_fn =
|
||||
EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
|
||||
// remove the layer
|
||||
@@ -871,6 +894,10 @@ impl<T: Item + 'static> Component for Picker<T> {
|
||||
self.completion_height = height.saturating_sub(4);
|
||||
Some((width, height))
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<&'static str> {
|
||||
Some(self.id)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
@@ -905,6 +932,8 @@ type PickerCallback<T> = Box<dyn Fn(&mut Context, &T, Action)>;
|
||||
pub type DynQueryCallback<T> =
|
||||
Box<dyn Fn(String, &mut Editor) -> BoxFuture<'static, anyhow::Result<Vec<T>>>>;
|
||||
|
||||
pub const DYNAMIC_PICKER_ID: &'static str = "dynamic-picker";
|
||||
|
||||
/// A picker that updates its contents via a callback whenever the
|
||||
/// query string changes. Useful for live grep, workspace symbols, etc.
|
||||
pub struct DynamicPicker<T: ui::menu::Item + Send> {
|
||||
@@ -914,8 +943,6 @@ pub struct DynamicPicker<T: ui::menu::Item + Send> {
|
||||
}
|
||||
|
||||
impl<T: ui::menu::Item + Send> DynamicPicker<T> {
|
||||
pub const ID: &'static str = "dynamic-picker";
|
||||
|
||||
pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
|
||||
Self {
|
||||
file_picker,
|
||||
@@ -947,10 +974,11 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
|
||||
let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| {
|
||||
// Wrapping of pickers in overlay is done outside the picker code,
|
||||
// so this is fragile and will break if wrapped in some other widget.
|
||||
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(Self::ID) {
|
||||
Some(overlay) => &mut overlay.content.file_picker,
|
||||
None => return,
|
||||
};
|
||||
let picker =
|
||||
match compositor.find_id::<Overlay<DynamicPicker<T>>>(DYNAMIC_PICKER_ID) {
|
||||
Some(overlay) => &mut overlay.content.file_picker,
|
||||
None => return,
|
||||
};
|
||||
picker.set_options(new_options);
|
||||
editor.reset_idle_timer();
|
||||
}));
|
||||
@@ -968,6 +996,92 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
|
||||
}
|
||||
|
||||
fn id(&self) -> Option<&'static str> {
|
||||
Some(Self::ID)
|
||||
Some(DYNAMIC_PICKER_ID)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close_buffer_in_buffer_picker(
|
||||
component: &mut dyn Component,
|
||||
cx: &mut Context,
|
||||
) -> EventResult {
|
||||
let Some(picker) = component
|
||||
.as_any_mut()
|
||||
.downcast_mut::<crate::commands::BufferPicker>()
|
||||
else {
|
||||
return EventResult::Ignored(None);
|
||||
};
|
||||
let Some(id) = picker.selection().map(|meta| meta.id) else {
|
||||
return EventResult::Ignored(None);
|
||||
};
|
||||
match cx.editor.close_document(id, false) {
|
||||
Ok(_) => {
|
||||
picker.options.retain(|item| item.id != id);
|
||||
if picker.options.is_empty() {
|
||||
return close_fn();
|
||||
}
|
||||
picker.cursor = picker.cursor.saturating_sub(1);
|
||||
picker.force_score();
|
||||
}
|
||||
// TODO: impl From<CloseError> for anyhow::Error
|
||||
Err(_err) => cx.editor.set_error("Failed to close buffer"),
|
||||
}
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
// Above command is cool because it's for one specific picker.
|
||||
|
||||
// This is also cool because it doesn't even need to interact with
|
||||
// the picker, so we don't need concrete types:
|
||||
|
||||
pub fn close_picker(_component: &mut dyn Component, _cx: &mut Context) -> EventResult {
|
||||
close_fn()
|
||||
}
|
||||
|
||||
// Now this is a problem. It compiles ok.
|
||||
// We can probably even specify it in the default keymap:
|
||||
//
|
||||
// MappableCommand::Component { name: "..", doc: "..", fun: crate::ui::picker::to_start<PathBuf> }
|
||||
//
|
||||
// But how do we represent this in keymap config? Do we do namespacing in the
|
||||
// command names and end up with tens of commands for scrolling each picker?
|
||||
//
|
||||
// MappableCommand::Component {
|
||||
// name: "file_picker::to_start",
|
||||
// doc: "..",
|
||||
// crate::ui::picker::to_start<PathBuf>,
|
||||
// },
|
||||
// MappableCommand::Component {
|
||||
// name: "buffer_picker::to_start",
|
||||
// doc: "..",
|
||||
// crate::ui::picker::to_start<BufferMeta>,
|
||||
// },
|
||||
//
|
||||
// Can we use a macro to close over the verbose parts of this?
|
||||
//
|
||||
// Can we do something clever with a hypothetical AnyPicker interface
|
||||
// similar to AnyComponent? Will we have to do that for every Component
|
||||
// that uses generics?
|
||||
|
||||
pub fn to_start<T: ui::menu::Item + 'static>(
|
||||
component: &mut dyn Component,
|
||||
_cx: &mut Context,
|
||||
) -> EventResult {
|
||||
let Some(picker) = component
|
||||
.as_any_mut()
|
||||
.downcast_mut::<Picker<T>>()
|
||||
else {
|
||||
return EventResult::Ignored(None);
|
||||
};
|
||||
|
||||
picker.cursor = 0;
|
||||
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
|
||||
fn close_fn() -> EventResult {
|
||||
EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
|
||||
// remove the layer
|
||||
compositor.last_picker = compositor.pop();
|
||||
})))
|
||||
}
|
||||
|
Reference in New Issue
Block a user