Compare commits

...

5 Commits

Author SHA1 Message Date
Michael Davis
136704b3fb WIP explore more commands on generic types 2023-06-25 14:45:21 -05:00
Michael Davis
71b6cc4d17 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?
2023-06-25 14:45:21 -05:00
Michael Davis
5ca3ed3ef8 Expose IDs to be used for parsing component names
This is just the picker for now but could be expanded to other
components.
2023-06-25 14:32:23 -05:00
Michael Davis
21169c77fd keymap: Allow looking up by Component ID
Previously we only split key tries by editor Mode. This commit changes
that to allow other "domain"s like Component IDs. We'll use the
Component IDs later so that a Component like a Picker can look up
keybindings for itself.

The naming is a bit of a placeholder. The hashmap could just use
`&'static str` and we could add a From for Mode. But I'd like the key to
have some sort of formal name to it.
2023-06-25 14:32:20 -05:00
Michael Davis
d6fc6a54b2 Move Keymaps from EditorView to Context structs
We'll need this value when attempting to execute keymaps for other
Components. Previously it was only scoped to the EditorView.
2023-06-25 14:32:11 -05:00
9 changed files with 261 additions and 70 deletions

View File

@@ -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,

View File

@@ -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)));
}
fn buffer_picker(cx: &mut Context) {
let current = view!(cx.editor).doc;
struct BufferMeta {
id: DocumentId,
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;
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,

View File

@@ -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,

View File

@@ -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,
}),
},

View File

@@ -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(),

View File

@@ -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,
)
}

View File

@@ -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 {

View File

@@ -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};

View File

@@ -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,7 +974,8 @@ 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) {
let picker =
match compositor.find_id::<Overlay<DynamicPicker<T>>>(DYNAMIC_PICKER_ID) {
Some(overlay) => &mut overlay.content.file_picker,
None => return,
};
@@ -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();
})))
}