Unnamed repository; edit this file 'description' to name the repository.
Implement a Component command for closing a buffer picker buffer
This is a basic example of a remappable command specific to a single Component. It could be remapped like so: ```toml [keys.buffer-picker] C-d = "close_buffer_in_buffer_picker" ``` This has some rough edges: * Can we namespace the commands so they don't all have to be very long and specific about which component they work for? * How can we make this work for generics? * We can't define commands that operate on a `Picker<_>` for example. This example only works because we're using a `Picker<BufferMeta>`. * For Pickers and Menus we could use a `Vec<Box<dyn Item>>` and drop the generics but that would lose static dispatch. * Could we separate the part that needs generics into a different struct and have the functions operate on that?
Michael Davis 2023-06-26
parent 5ca3ed3 · commit 71b6cc4
-rw-r--r--helix-term/src/commands.rs38
-rw-r--r--helix-term/src/keymap/default.rs6
-rw-r--r--helix-term/src/ui/picker.rs48
3 files changed, 83 insertions, 9 deletions
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index f829e4d5..daf961d1 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -50,7 +50,7 @@ use movement::Movement;
use crate::{
args,
- compositor::{self, Component, Compositor},
+ compositor::{self, Component, Compositor, EventResult},
filter_picker_entry,
job::Callback,
keymap::{Keymaps, ReverseKeymap},
@@ -172,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 {
@@ -209,6 +214,7 @@ impl MappableCommand {
}
}
Self::Static { fun, .. } => (fun)(cx),
+ Self::Component { .. } => unimplemented!(),
}
}
@@ -216,6 +222,7 @@ impl MappableCommand {
match &self {
Self::Typable { name, .. } => name,
Self::Static { name, .. } => name,
+ Self::Component { .. } => unimplemented!(),
}
}
@@ -223,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",
@@ -503,6 +519,7 @@ impl fmt::Debug for MappableCommand {
.field(name)
.field(args)
.finish(),
+ Self::Component { .. } => unimplemented!(),
}
}
}
@@ -2526,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 = ();
@@ -2710,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!(),
}
}
}
diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs
index fd92c361..7bd5146f 100644
--- a/helix-term/src/keymap/default.rs
+++ b/helix-term/src/keymap/default.rs
@@ -379,9 +379,15 @@ pub fn default() -> HashMap<Domain, KeyTrie> {
"home" => goto_line_start,
"end" => goto_line_end_newline,
});
+
+ let buffer_picker = keymap!({ "Buffer picker"
+ "C-x" => close_buffer_in_buffer_picker,
+ });
+
hashmap!(
Domain::Mode(Mode::Normal) => normal,
Domain::Mode(Mode::Select) => select,
Domain::Mode(Mode::Insert) => insert,
+ Domain::Component("buffer-picker") => buffer_picker,
)
}
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index 192a03a6..dea2e905 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -802,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
@@ -987,3 +999,39 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
Some(DYNAMIC_PICKER_ID)
}
}
+
+pub fn close_buffer_in_buffer_picker(
+ component: &mut dyn Component,
+ cx: &mut compositor::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)
+}
+
+fn close_fn() -> EventResult {
+ EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
+ // remove the layer
+ compositor.last_picker = compositor.pop();
+ })))
+}