Skip to content

Commit

Permalink
doc: document Menu and Layer
Browse files Browse the repository at this point in the history
  • Loading branch information
justinpombrio committed Apr 13, 2024
1 parent 21480cb commit e90df96
Show file tree
Hide file tree
Showing 3 changed files with 81 additions and 65 deletions.
59 changes: 29 additions & 30 deletions src/keymap/keymap.rs
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ struct KeyProg {
}

impl KeyProg {
// If this KeyProg is from a general binding, `candidate` should be None.
fn to_program(&self, candidate: Option<&Candidate>) -> Prog {
let mut prog = self.prog.to_owned();
if let Some(value) = candidate.and_then(|candidate| candidate.value()) {
Expand Down Expand Up @@ -113,11 +114,10 @@ impl KeyProg {
/// ```text
/// +------------------+
/// | prompt> |
/// +------------------+
/// | [new file] |
/// -> | baz.rs |
/// | foobar.rs |
/// | .. |
/// | * [new file] |
/// -> | * baz.rs |
/// | * foobar.rs |
/// | * .. |
/// +------------------+
/// | enter: open |
/// | ctrl-d: delete |
Expand All @@ -129,48 +129,48 @@ impl KeyProg {
///
/// - A prompt where the user can enter text. It can be used to filter the candidates or to
/// create a new file with a custom name.
///
/// - A list of candidates. This includes the name of each file in the directory, a special
/// candidate ".." that opens a new selection menu for the parent directory, and a custom candidate
/// candidate `..` that opens a new selection menu for the parent directory, and a custom candidate
/// that creates a new file with whatever name was entered at the prompt. There is always one
/// selected candidate, shown here with an "->".
/// selected candidate, shown here with `->`.
///
/// - A list of key hints, showing which keys can be pressed and what they will do. These change
/// depending on which candidate is currently selected. For example, normal files can be "opened" or
/// "deleted", `..` can be "opened", and the custom candidate can be "created".
///
/// If the user types "foo" and presses the up arrow, the candidates list will be filtered, the
/// selection will move to the custom "[new file]" candidate, and the key hints will be updated to
/// reflect that selection:
/// selection will move to the custom "[new file] foo" candidate, and the key hints will be updated
/// to reflect that selection:
///
/// ```text
/// +------------------+
/// |prompt> foo |
/// | prompt> foo |
/// -> | * [new file] foo |
/// | * foobar.rs |
/// +------------------+
/// -> |[new file] foo |
/// |foobar.rs |
/// +------------------+
/// | enter: create |
/// | esc: exit menu |
/// | enter: create |
/// | esc: exit menu |
/// +------------------+
/// ```
///
/// Many keymaps don't need candidate selection. These will only contain _general bindings_, added
/// with the method [`add_binding()`]. This method binds a key to a function that takes no
/// arguments. If you only add bindings with this method, then no prompt or candidate list will be
/// shown.
/// arguments. If you only add general bindings, then no prompt or candidate list will be shown.
///
/// For keymaps with candidate selection, there are three kinds of candidates:
///
/// - _The custom candidate._ The method [`bind_key_for_custom_candidate()`] binds a key to a
/// function that takes the user's input string as an argument. The custom candidate is only shown
/// in the list if there is at least one binding for it. In the file example, `[new file]` is a
/// custom candidate.
/// in the list if there is at least one binding for it. In the file example, the entry prefixed
/// with `[new file]` is a custom candidate.
///
/// - _Regular candidates._ The method [`add_regular_candidate`] adds a regular candidate to the
/// - _Regular candidates._ The method [`add_regular_candidate()`] adds a regular candidate to the
/// candidate list. This candidate has both a display string and a _value_. The method
/// [`bind_key_for_regular_candidates()`] binds a key to a function that takes the selected
/// candidate's value as an argument. Each of these bindings applies to _all_ regular candidates.
/// In the file example, the file names "baz.rs" and "foobar.rs" are regular candidates and "enter"
/// is bound to "open file by name" for both of them.
/// candidate's value as an argument. Each such binding applies to _all_ regular candidates. In
/// the file example, the file names "baz.rs" and "foobar.rs" are regular candidates and "enter" is
/// bound to "open file by name" for both of them.
///
/// - _Special candidates._ The method [`bind_key_for_special_candidate()`] adds a special
/// candidate to the candidate list, and gives it a binding from a key to a function that takes no
Expand All @@ -180,8 +180,7 @@ impl KeyProg {
///
/// You can have both candidate selection and general bindings in one keymap. In the file example,
/// "esc" is bound to "exit menu" with a general binding. The key hints pane shows the key bindings
/// for the selected candidate, plus all general key bindings. (If the same key is bound for both,
/// the candidate specific binding overrides the general binding.)
/// for the selected candidate, plus all general key bindings.
///
/// ### Conflict Resolution
///
Expand All @@ -199,14 +198,14 @@ pub struct Keymap {
general_bindings: OrderedMap<Key, KeyProg>,
/// If the user types `Key` while `String` is selected, execute `KeyProg`.
special_bindings: OrderedMap<String, OrderedMap<Key, KeyProg>>,
/// If the user types `Key` while any of `regular_candidates` is selected, push the regular candidate's
/// `Value` and then execute `KeyProg`.
/// If the user types `Key` while any of `regular_candidates` is selected, invoke `KeyProg`
/// with the regular candidate's `Value`.
regular_bindings: OrderedMap<Key, KeyProg>,
/// The set of regular candidates. Each has a display label and a value to push.
/// The set of regular candidates. Each has a display label and a value.
// TODO: Regular candidate insertion is quadratic. Make an efficient OrderedSet instead.
regular_candidates: Vec<(String, Value)>,
/// If the user types `Key` while a custom candidate is selected, push its string and execute
/// `KeyProg`.
/// If the user types `Key` while a custom candidate is selected, invoke `KeyProg` with the
/// user's input string.
custom_bindings: OrderedMap<Key, KeyProg>,
}

Expand Down
62 changes: 37 additions & 25 deletions src/keymap/layer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@ use crate::tree::Node;
use crate::util::IndexedMap;
use std::collections::HashMap;

// TODO:
// - filtering by sort
// - docs
// - proofread keymap & layer
// - add tests

type LayerIndex = usize;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
Expand All @@ -27,7 +21,8 @@ enum KeymapLabel {
* Layer *
*********/

// TODO: doc
/// A collection of Keymaps, with up to one Keymap per `MenuName` or `Mode`. Layers can be stacked
/// on top of each other to combine their functionality; see [`LayerManager`].
#[derive(Debug, Clone)]
pub struct Layer {
name: String,
Expand All @@ -42,14 +37,15 @@ impl Layer {
}
}

pub fn add_menu(&mut self, menu_name: MenuName, keymap: Keymap) {
pub fn add_menu_keymap(&mut self, menu_name: MenuName, keymap: Keymap) {
self.keymaps.insert(KeymapLabel::Menu(menu_name), keymap);
}

pub fn add_mode(&mut self, mode: Mode, keymap: Keymap) {
pub fn add_mode_keymap(&mut self, mode: Mode, keymap: Keymap) {
self.keymaps.insert(KeymapLabel::Mode(mode), keymap);
}

// If the same KeymapLabel is used in multiple layers, later layers override earlier layers
fn merge(name: String, layers: impl IntoIterator<Item = Layer>) -> Layer {
let mut keymaps = HashMap::<KeymapLabel, Keymap>::new();
for layer in layers {
Expand All @@ -69,10 +65,12 @@ impl Layer {
* LayerManager *
****************/

/// Manage keymap layers.
/// Manage [`Layer`]s of [`Keymap`]s, and track the active menu.
///
/// Layers added later has priority over layers added earlier,
/// though every local layer has priority over every global layer.
/// Layers can be stacked on top of each other. There is a global stack of layers that apply to all
/// documents, as well as a local stack of layers for each individual document. When different
/// layers have conflicting bindings, layers higher in the stack take priority over lower layers,
/// and local layers take priority over global layers.
pub struct LayerManager {
global_layers: Vec<LayerIndex>,
local_layers: HashMap<DocName, Vec<LayerIndex>>,
Expand All @@ -96,62 +94,74 @@ impl LayerManager {
* Layers *
**********/

/// Register the layer, so that it can later be added or removed by name.
pub fn register_layer(&mut self, layer: Layer) {
self.layers.insert(layer.name.clone(), layer);
}

/// Add a global keymap layer. Returns `Err` if the layer has not been registered.
/// Add a global keymap layer to the top of the global layer stack. Returns `Err` if the layer
/// has not been registered.
pub fn add_global_layer(&mut self, layer_name: &str) -> Result<(), ()> {
add_layer(&self.layers, &mut self.global_layers, layer_name)?;
self.cached_composite_layers.clear();
Ok(())
}

/// Add a keymap layer for this document. Returns `Err` if the layer has not been registered.
/// Add a keymap layer to the top of the given document's local layer stack. Returns `Err` if
/// the layer has not been registered.
pub fn add_local_layer(&mut self, doc_name: &DocName, layer_name: &str) -> Result<(), ()> {
let mut local_layers = self.local_layers.entry(doc_name.to_owned()).or_default();
add_layer(&self.layers, local_layers, layer_name)?;
self.cached_composite_layers.clear();
Ok(())
}

/// Remove a global keymap layer. Returns `Err` if the layer has not been registered.
/// Remove a global keymap layer from wherever it is in the global layer stack. Returns `Err`
/// if the layer has not been registered.
pub fn remove_global_layer(&mut self, layer_name: &str) -> Result<(), ()> {
remove_layer(&self.layers, &mut self.global_layers, layer_name)?;
self.cached_composite_layers.clear();
Ok(())
}

/// Remove a keymap layer for this document. Returns `Err` if the layer has not been
/// registered.
/// Remove a keymap layer from wherever it is in the given document's local layer stack.
/// Returns `Err` if the layer has not been registered.
pub fn remove_local_layer(&mut self, doc_name: &DocName, layer_name: &str) -> Result<(), ()> {
let mut local_layers = self.local_layers.entry(doc_name.to_owned()).or_default();
remove_layer(&self.layers, local_layers, layer_name)?;
self.cached_composite_layers.clear();
Ok(())
}

/// Iterate over all active global layer names.
/// Iterate over all global layer names, from the bottom to top of the stack.
pub fn global_layers(&self) -> impl Iterator<Item = &str> {
self.global_layers
.iter()
.map(|i| self.layers[*i].name.as_ref())
}

/// Iterate over all active local layer names for the given document.
/// Iterate over all local layer names for the given document, from the bottom to top of the
/// stack.
pub fn local_layers(&self, doc_name: &DocName) -> impl Iterator<Item = &str> {
self.local_layers
.get(doc_name)
.into_iter()
.flat_map(|layers| layers.iter().map(|i| self.layers[*i].name.as_ref()))
}

/// Iterate over all registered layer names, regardless of whether they're currently in use, in
/// arbitrary order.
pub fn all_layers(&self) -> impl Iterator<Item = &str> {
self.layers.names()
}

/*********
* Menus *
*********/

/// Open the named menu. If `dynamic_keymap` is `Some`, layer it on top of the existing
/// keymaps for the menu. Returns `false` if none of the layers have a menu of this name.
/// Open the named menu. If `dynamic_keymap` is `Some`, layer it on top of the existing keymaps
/// for the menu. Returns `false` and does nothing if there's no menu to open (this happens
/// when none of the layers have a menu of this name and `dynamic_keymap` is `None`).
#[must_use]
pub fn enter_menu(
&mut self,
Expand Down Expand Up @@ -179,8 +189,8 @@ impl LayerManager {
self.active_menu = None;
}

/// Edit the menu input selection.
/// Returns `false` if there is no menu open, or it does not have candidate selection.
/// Manipulate the menu's candidate selection. Returns `false` and does nothing if there is no
/// menu open, or it does not have candidate selection.
#[must_use]
pub fn edit_menu_selection(&mut self, cmd: MenuSelectionCmd) -> bool {
if let Some(menu) = &mut self.active_menu {
Expand All @@ -194,6 +204,8 @@ impl LayerManager {
* Input *
*********/

/// Lookup the program to run when the given key is pressed, given the current mode and active
/// document.
pub fn lookup_key(&mut self, mode: Mode, doc_name: Option<&DocName>, key: Key) -> Option<Prog> {
if let Some(menu) = &self.active_menu {
menu.lookup(key)
Expand All @@ -208,10 +220,10 @@ impl LayerManager {
* Display *
***********/

pub fn make_selection_doc(&self, s: &mut Storage) -> Option<Node> {
pub fn make_candidate_selection_doc(&self, s: &mut Storage) -> Option<Node> {
self.active_menu
.as_ref()
.and_then(|menu| menu.make_selection_doc(s))
.and_then(|menu| menu.make_candidate_selection_doc(s))
}

pub fn make_keyhint_doc(
Expand Down
25 changes: 15 additions & 10 deletions src/keymap/menu.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,22 @@ const SELECTION_LANGUAGE_NAME: &str = "SelectionMenu";

pub type MenuName = String;

/// A command that manipulates the menu's candidate selection.
pub enum MenuSelectionCmd {
Up,
Down,
Backspace,
Insert(char),
}

/// An open menu. Keeps track of the state of its candidate selection.
pub struct Menu {
name: MenuName,
keymap: Keymap,
selection: Option<MenuSelection>,
}

/// The state of a menu's candidate selection.
struct MenuSelection {
custom_candidate: Option<Candidate>,
candidates: Vec<Candidate>,
Expand Down Expand Up @@ -84,7 +87,13 @@ impl MenuSelection {
filtered
}

fn make_selection_doc(&self, s: &mut Storage) -> Node {
fn selected_candidate(&self) -> Option<&Candidate> {
let candidates = self.filtered_candidates();
let index = self.index.min(candidates.len().saturating_sub(1));
candidates.get(index).copied()
}

fn make_candidate_selection_doc(&self, s: &mut Storage) -> Node {
use Candidate::{Custom, Regular, Special};

// Lookup SelectionMenu language and constructs
Expand Down Expand Up @@ -148,23 +157,19 @@ impl Menu {
self.keymap.lookup(key, self.selected_candidate())
}

pub fn make_selection_doc(&self, s: &mut Storage) -> Option<Node> {
pub fn make_candidate_selection_doc(&self, s: &mut Storage) -> Option<Node> {
self.selection
.as_ref()
.map(|selection| selection.make_selection_doc(s))
.map(|selection| selection.make_candidate_selection_doc(s))
}

pub fn make_keyhint_doc(&self, s: &mut Storage) -> Node {
self.keymap.make_keyhint_doc(s, self.selected_candidate())
}

fn selected_candidate(&self) -> Option<&Candidate> {
if let Some(selection) = &self.selection {
let candidates = selection.filtered_candidates();
let index = selection.index.min(candidates.len().saturating_sub(1));
candidates.get(index).copied()
} else {
None
}
self.selection
.as_ref()
.and_then(|selection| selection.selected_candidate())
}
}

0 comments on commit e90df96

Please sign in to comment.