use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use ratatui::style::Color;
use crate::app::View;
use crate::ui::views::InputMode;
pub fn is_refresh_key(key: &KeyEvent) -> bool {
matches!(key.code, KeyCode::Char('l') | KeyCode::Char('L'))
&& key.modifiers.contains(KeyModifiers::CONTROL)
}
pub const QUIT: KeyCode = KeyCode::Char('q');
pub const HELP: KeyCode = KeyCode::Char('?');
pub const TAB: KeyCode = KeyCode::Tab;
pub const ESC: KeyCode = KeyCode::Esc;
pub const MOVE_UP: KeyCode = KeyCode::Char('k');
pub const MOVE_UP_ARROW: KeyCode = KeyCode::Up;
pub const MOVE_DOWN: KeyCode = KeyCode::Char('j');
pub const MOVE_DOWN_ARROW: KeyCode = KeyCode::Down;
pub const GO_TOP: KeyCode = KeyCode::Char('g');
pub const GO_BOTTOM: KeyCode = KeyCode::Char('G');
pub fn is_move_up(code: KeyCode) -> bool {
matches!(code, MOVE_UP | MOVE_UP_ARROW)
}
pub fn is_move_down(code: KeyCode) -> bool {
matches!(code, MOVE_DOWN | MOVE_DOWN_ARROW)
}
pub const SUBMIT: KeyCode = KeyCode::Enter;
pub const OPEN_DIFF: KeyCode = KeyCode::Enter;
pub const DESCRIBE: KeyCode = KeyCode::Char('d');
pub const EDIT: KeyCode = KeyCode::Char('e');
pub const NEW_CHANGE: KeyCode = KeyCode::Char('c');
pub const COMMIT: KeyCode = KeyCode::Char('C');
pub const SQUASH: KeyCode = KeyCode::Char('S');
pub const ABANDON: KeyCode = KeyCode::Char('A');
pub const SPLIT: KeyCode = KeyCode::Char('x');
pub const OBJECT_DELETE: KeyCode = KeyCode::Char('d');
pub const OBJECT_NEW: KeyCode = KeyCode::Char('n');
pub const REBASE: KeyCode = KeyCode::Char('R');
pub const ABSORB: KeyCode = KeyCode::Char('B');
pub const NEW_FROM: KeyCode = KeyCode::Char('C');
pub const ANNOTATE: KeyCode = KeyCode::Char('a');
pub const RESOLVE_LIST: KeyCode = KeyCode::Char('X');
pub const FETCH: KeyCode = KeyCode::Char('F');
pub const PUSH: KeyCode = KeyCode::Char('P');
pub const TRACK: KeyCode = KeyCode::Char('t');
pub const COMPARE: KeyCode = KeyCode::Char('=');
pub const INTERDIFF: KeyCode = KeyCode::Char('I');
pub const BISECT: KeyCode = KeyCode::Char('W');
pub const METAEDIT: KeyCode = KeyCode::Char('v');
pub const TAG_VIEW: KeyCode = KeyCode::Char('t');
pub const WORKSPACE_VIEW: KeyCode = KeyCode::Char('w');
pub const BOOKMARK_VIEW: KeyCode = KeyCode::Char('b');
pub const COMMAND_HISTORY: KeyCode = KeyCode::Char('H');
pub const PREVIEW: KeyCode = KeyCode::Char('p');
pub const BOOKMARK_UNTRACK: KeyCode = KeyCode::Char('T');
pub const OBJECT_RENAME: KeyCode = KeyCode::Char('r');
pub const BOOKMARK_FORGET: KeyCode = KeyCode::Char('f');
pub const BOOKMARK_MOVE: KeyCode = KeyCode::Char('m');
pub const COMMAND_PALETTE: KeyCode = KeyCode::Char(':');
pub const NEXT_CHANGE: KeyCode = KeyCode::Char(']');
pub const PREV_CHANGE: KeyCode = KeyCode::Char('[');
pub const LOG_REVERSE: KeyCode = KeyCode::Char('V');
pub const DUPLICATE: KeyCode = KeyCode::Char('Y');
pub const DIFFEDIT: KeyCode = KeyCode::Char('E');
pub const RESTORE_FILE: KeyCode = KeyCode::Char('r');
pub const RESTORE_ALL: KeyCode = KeyCode::Char('R');
pub const EVOLOG: KeyCode = KeyCode::Char('L');
pub const REVERT: KeyCode = KeyCode::Char('Z');
pub const SIMPLIFY_PARENTS: KeyCode = KeyCode::Char('i');
pub const FIX: KeyCode = KeyCode::Char('f');
pub const PARALLELIZE: KeyCode = KeyCode::Char('|');
pub const ARRANGE: KeyCode = KeyCode::Char('O');
pub const JUMP_CONFLICT: KeyCode = KeyCode::Char('f');
pub const SEARCH_INPUT: KeyCode = KeyCode::Char('/');
pub const REVSET_INPUT: KeyCode = KeyCode::Char('r');
pub const SEARCH_NEXT: KeyCode = KeyCode::Char('n');
pub const SEARCH_PREV: KeyCode = KeyCode::Char('N');
pub const NEXT_FILE: KeyCode = KeyCode::Char(']');
pub const PREV_FILE: KeyCode = KeyCode::Char('[');
pub const HALF_PAGE_DOWN: KeyCode = KeyCode::Char('d');
pub const HALF_PAGE_UP: KeyCode = KeyCode::Char('u');
pub const YANK: KeyCode = KeyCode::Char('y');
pub const YANK_DIFF: KeyCode = KeyCode::Char('Y');
pub const WRITE_FILE: KeyCode = KeyCode::Char('w');
pub const DIFF_FORMAT_CYCLE: KeyCode = KeyCode::Char('m');
pub const DIFF_DESC_TOGGLE: KeyCode = KeyCode::Char('t');
pub const UNDO: KeyCode = KeyCode::Char('u');
pub const STATUS_VIEW: KeyCode = KeyCode::Char('s');
pub const OPERATION_HISTORY: KeyCode = KeyCode::Char('o');
pub struct KeyBindEntry {
pub key: &'static str,
pub description: &'static str,
}
pub const GLOBAL_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "q",
description: "Quit / Back",
},
KeyBindEntry {
key: "?",
description: "Help",
},
KeyBindEntry {
key: "Tab",
description: "Switch view",
},
KeyBindEntry {
key: "Esc",
description: "Back to previous",
},
KeyBindEntry {
key: "Ctrl+l",
description: "Refresh",
},
];
pub const NAV_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
];
pub const LOG_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: ":",
description: "Command palette (search low-frequency commands)",
},
KeyBindEntry {
key: "Enter",
description: "Show diff",
},
KeyBindEntry {
key: "d",
description: "Describe (1-line quick edit; opens editor for multi-line)",
},
KeyBindEntry {
key: "Ctrl+e",
description: "Describe in external editor (full text)",
},
KeyBindEntry {
key: "e",
description: "Edit change",
},
KeyBindEntry {
key: "c",
description: "Create new change",
},
KeyBindEntry {
key: "C",
description: "New from selected (Log)",
},
KeyBindEntry {
key: "/",
description: "Search in list",
},
KeyBindEntry {
key: "r",
description: "Revset filter",
},
KeyBindEntry {
key: "n/N",
description: "Next/prev search",
},
KeyBindEntry {
key: "s",
description: "Status view",
},
KeyBindEntry {
key: "o",
description: "Operation history",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "S",
description: "Squash (select target)",
},
KeyBindEntry {
key: "A",
description: "Abandon change",
},
KeyBindEntry {
key: "x",
description: "Split change",
},
KeyBindEntry {
key: "b",
description: "Bookmark view",
},
KeyBindEntry {
key: "R",
description: "Rebase (r/s/b/A/B)",
},
KeyBindEntry {
key: "B",
description: "Absorb changes",
},
KeyBindEntry {
key: "X",
description: "Resolve conflicts",
},
KeyBindEntry {
key: "F",
description: "Git fetch",
},
KeyBindEntry {
key: "P",
description: "Git push",
},
KeyBindEntry {
key: "t",
description: "Tag view",
},
KeyBindEntry {
key: "w",
description: "Workspace view",
},
KeyBindEntry {
key: "p",
description: "Toggle preview pane",
},
KeyBindEntry {
key: "]/[",
description: "Move @ to next/prev",
},
KeyBindEntry {
key: "V",
description: "Toggle reversed order",
},
KeyBindEntry {
key: "L",
description: "Evolution log (change history)",
},
KeyBindEntry {
key: "H",
description: "Command history",
},
];
pub const INPUT_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "Enter",
description: "Submit input",
},
KeyBindEntry {
key: "Esc",
description: "Cancel input",
},
KeyBindEntry {
key: "Backspace",
description: "Delete character",
},
];
pub const DIFF_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "m",
description: "Cycle diff display mode (color-words/stat/git)",
},
KeyBindEntry {
key: "t",
description: "Toggle full description (expand/collapse header)",
},
KeyBindEntry {
key: "j/k",
description: "Scroll down/up",
},
KeyBindEntry {
key: "d/u",
description: "Half page down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "[/]",
description: "Prev/next file",
},
KeyBindEntry {
key: "a",
description: "Show file blame",
},
KeyBindEntry {
key: "y",
description: "Copy to clipboard (full: jj show)",
},
KeyBindEntry {
key: "Y",
description: "Copy to clipboard (diff only: jj diff)",
},
KeyBindEntry {
key: "w",
description: "Export to .patch file",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
#[derive(Clone, Copy)]
pub struct KeyHint {
pub key: &'static str,
pub label: &'static str,
pub color: Color,
}
pub const HINT_HELP: KeyHint = KeyHint {
key: "?",
label: "Help",
color: Color::Cyan,
};
pub const HINT_DESC: KeyHint = KeyHint {
key: "d",
label: "Desc",
color: Color::Green,
};
pub const HINT_EDITOR: KeyHint = KeyHint {
key: "^E",
label: "Editor",
color: Color::Green,
};
pub const HINT_EDIT: KeyHint = KeyHint {
key: "e",
label: "Edit",
color: Color::Yellow,
};
pub const HINT_NEW: KeyHint = KeyHint {
key: "c",
label: "New",
color: Color::Magenta,
};
pub const HINT_NEW_AT: KeyHint = KeyHint {
key: "C",
label: "New@",
color: Color::Magenta,
};
pub const HINT_SQUASH: KeyHint = KeyHint {
key: "S",
label: "Squash",
color: Color::Red,
};
pub const HINT_ABANDON: KeyHint = KeyHint {
key: "A",
label: "Abandon",
color: Color::Red,
};
pub const HINT_SPLIT: KeyHint = KeyHint {
key: "x",
label: "Split",
color: Color::Yellow,
};
pub const HINT_BOOKMARK: KeyHint = KeyHint {
key: "b",
label: "Bookmark",
color: Color::Cyan,
};
pub const HINT_PALETTE: KeyHint = KeyHint {
key: ":",
label: "Palette",
color: Color::Magenta,
};
pub const HINT_DEL_BKM: KeyHint = KeyHint {
key: "d",
label: "Delete",
color: Color::Red,
};
pub const HINT_REBASE: KeyHint = KeyHint {
key: "R",
label: "Rebase",
color: Color::Yellow,
};
pub const HINT_ABSORB: KeyHint = KeyHint {
key: "B",
label: "Absorb",
color: Color::Magenta,
};
pub const HINT_RESOLVE: KeyHint = KeyHint {
key: "X",
label: "Resolve",
color: Color::Red,
};
pub const HINT_FETCH: KeyHint = KeyHint {
key: "F",
label: "Fetch",
color: Color::Blue,
};
pub const HINT_PUSH: KeyHint = KeyHint {
key: "P",
label: "Push",
color: Color::Blue,
};
pub const HINT_TRACK: KeyHint = KeyHint {
key: "t",
label: "Track",
color: Color::Cyan,
};
pub const HINT_UNDO: KeyHint = KeyHint {
key: "u",
label: "Undo",
color: Color::Green,
};
pub const HINT_REDO: KeyHint = KeyHint {
key: "^R",
label: "Redo",
color: Color::Green,
};
pub const HINT_REFRESH: KeyHint = KeyHint {
key: "^L",
label: "Refresh",
color: Color::Blue,
};
pub const HINT_SWITCH: KeyHint = KeyHint {
key: "Tab",
label: "Switch",
color: Color::Blue,
};
pub const HINT_QUIT: KeyHint = KeyHint {
key: "q",
label: "Quit",
color: Color::Red,
};
pub const HINT_BACK: KeyHint = KeyHint {
key: "q",
label: "Back",
color: Color::Red,
};
pub const HINT_NAV: KeyHint = KeyHint {
key: "j/k",
label: "Navigate",
color: Color::Blue,
};
pub const HINT_SQUASH_CONFIRM: KeyHint = KeyHint {
key: "Enter",
label: "Squash",
color: Color::Green,
};
pub const HINT_CANCEL: KeyHint = KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
};
pub const HINT_SUBMIT: KeyHint = KeyHint {
key: "Enter",
label: "Confirm",
color: Color::Green,
};
pub const HINT_CANCEL_ESC: KeyHint = KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
};
pub const HINT_YES: KeyHint = KeyHint {
key: "y/Enter",
label: "Yes",
color: Color::Green,
};
pub const HINT_NO: KeyHint = KeyHint {
key: "n/Esc",
label: "No",
color: Color::Red,
};
pub const HINT_TOGGLE: KeyHint = KeyHint {
key: "Space",
label: "Toggle",
color: Color::Yellow,
};
pub const HINT_CONFIRM: KeyHint = KeyHint {
key: "Enter",
label: "Confirm",
color: Color::Green,
};
pub const HINT_DIALOG_CANCEL: KeyHint = KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
};
pub const HINT_SELECT: KeyHint = KeyHint {
key: "Enter",
label: "Select",
color: Color::Green,
};
pub const HINT_RESOLVE_ENTER: KeyHint = KeyHint {
key: "Enter",
label: "Resolve",
color: Color::Green,
};
pub const HINT_OURS: KeyHint = KeyHint {
key: "o",
label: "Ours",
color: Color::Cyan,
};
pub const HINT_THEIRS: KeyHint = KeyHint {
key: "t",
label: "Theirs",
color: Color::Cyan,
};
pub const HINT_DIFF: KeyHint = KeyHint {
key: "d",
label: "Diff",
color: Color::Magenta,
};
pub const HINT_TAG_VIEW: KeyHint = KeyHint {
key: "t",
label: "Tags",
color: Color::Cyan,
};
pub const HINT_TAG_CREATE: KeyHint = KeyHint {
key: "n",
label: "Create Tag",
color: Color::Green,
};
pub const HINT_TAG_DELETE: KeyHint = KeyHint {
key: "d",
label: "Delete",
color: Color::Red,
};
pub const HINT_BKM_CREATE: KeyHint = KeyHint {
key: "n",
label: "Create",
color: Color::Green,
};
pub const HINT_JUMP_ENTER: KeyHint = KeyHint {
key: "Enter",
label: "Jump",
color: Color::Green,
};
pub const HINT_UNTRACK: KeyHint = KeyHint {
key: "T",
label: "Untrack",
color: Color::Yellow,
};
pub const HINT_LOG_JUMP: KeyHint = KeyHint {
key: "J",
label: "Log Jump",
color: Color::Yellow,
};
pub const HINT_PREVIEW: KeyHint = KeyHint {
key: "p",
label: "Preview",
color: Color::Blue,
};
pub const HINT_REVERSE: KeyHint = KeyHint {
key: "V",
label: "Reverse",
color: Color::Yellow,
};
pub const HINT_RENAME: KeyHint = KeyHint {
key: "r",
label: "Rename",
color: Color::Yellow,
};
pub const HINT_FORGET: KeyHint = KeyHint {
key: "f",
label: "Forget",
color: Color::Red,
};
pub const HINT_MOVE_BKM: KeyHint = KeyHint {
key: "m",
label: "Move",
color: Color::Yellow,
};
pub const HINT_EVOLOG: KeyHint = KeyHint {
key: "L",
label: "Evolog",
color: Color::Cyan,
};
pub const HINT_RESTORE: KeyHint = KeyHint {
key: "r",
label: "Restore",
color: Color::Red,
};
pub const HINT_RESTORE_ALL: KeyHint = KeyHint {
key: "R",
label: "RestoreAll",
color: Color::Red,
};
pub const HINT_DETAIL: KeyHint = KeyHint {
key: "Enter",
label: "Detail",
color: Color::Green,
};
#[derive(Clone, Copy, Debug)]
pub enum BookmarkKind {
LocalJumpable,
LocalNoChange,
TrackedRemote,
UntrackedRemote,
}
#[derive(Default)]
pub struct HintContext {
pub has_bookmarks: bool,
pub has_conflicts: bool,
pub is_working_copy: bool,
pub dialog: Option<DialogHintKind>,
pub selected_bookmark_kind: Option<BookmarkKind>,
pub skip_emptied: bool,
pub simplify_parents: bool,
pub rebase_mode: crate::model::RebaseMode,
}
pub enum DialogHintKind {
Confirm,
Select,
SingleSelect,
}
pub fn current_hints(view: View, input_mode: InputMode, ctx: &HintContext) -> Vec<KeyHint> {
if let Some(ref kind) = ctx.dialog {
return dialog_hints(kind);
}
match view {
View::Log => log_hints(input_mode, ctx),
View::Resolve => resolve_hints(ctx),
View::Bookmark => bookmark_view_hints(ctx),
View::Tag => tag_view_hints(),
View::Workspace => workspace_view_hints(),
View::CommandHistory => command_history_hints(),
View::Status => STATUS_VIEW_HINTS.to_vec(),
View::Operation => OPERATION_VIEW_HINTS.to_vec(),
_ => vec![],
}
}
fn dialog_hints(kind: &DialogHintKind) -> Vec<KeyHint> {
match kind {
DialogHintKind::Confirm => vec![HINT_YES, HINT_NO],
DialogHintKind::Select => vec![HINT_NAV, HINT_TOGGLE, HINT_CONFIRM, HINT_DIALOG_CANCEL],
DialogHintKind::SingleSelect => vec![HINT_NAV, HINT_SELECT, HINT_DIALOG_CANCEL],
}
}
fn log_hints(input_mode: InputMode, ctx: &HintContext) -> Vec<KeyHint> {
match input_mode {
InputMode::Normal => log_normal_hints(ctx),
InputMode::SquashSelect => vec![HINT_NAV, HINT_SQUASH_CONFIRM, HINT_CANCEL],
InputMode::RebaseModeSelect => REBASE_MODE_SELECT_HINTS.to_vec(),
InputMode::RebaseSelect => {
rebase_select_hints(ctx.skip_emptied, ctx.simplify_parents, ctx.rebase_mode)
}
InputMode::CompareSelect => COMPARE_SELECT_HINTS.to_vec(),
InputMode::InterdiffSelect => INTERDIFF_SELECT_HINTS.to_vec(),
InputMode::BisectSelect => BISECT_SELECT_HINTS.to_vec(),
InputMode::ParallelizeSelect => PARALLELIZE_SELECT_HINTS.to_vec(),
InputMode::RebaseRevsetInput => vec![HINT_SUBMIT, HINT_CANCEL_ESC],
InputMode::SearchInput
| InputMode::RevsetInput
| InputMode::DescribeInput
| InputMode::BookmarkInput => vec![HINT_SUBMIT, HINT_CANCEL_ESC],
}
}
fn log_normal_hints(ctx: &HintContext) -> Vec<KeyHint> {
let mut h = vec![
HINT_HELP,
HINT_PALETTE,
HINT_DESC,
HINT_EDIT,
HINT_NEW,
HINT_SQUASH,
HINT_ABANDON,
HINT_BOOKMARK,
HINT_REBASE,
HINT_FETCH,
];
if ctx.has_conflicts {
h.push(HINT_RESOLVE);
}
if ctx.has_bookmarks {
h.push(HINT_PUSH);
}
h.extend([HINT_UNDO, HINT_REDO, HINT_QUIT]);
h
}
fn undo_redo_refresh_back() -> [KeyHint; 4] {
[HINT_UNDO, HINT_REDO, HINT_REFRESH, HINT_BACK]
}
fn resolve_hints(ctx: &HintContext) -> Vec<KeyHint> {
let mut h = vec![HINT_HELP];
if ctx.is_working_copy {
h.push(HINT_RESOLVE_ENTER);
}
h.extend([
HINT_OURS,
HINT_THEIRS,
HINT_DIFF,
HINT_UNDO,
HINT_REDO,
HINT_REFRESH,
HINT_BACK,
]);
h
}
fn bookmark_view_hints(ctx: &HintContext) -> Vec<KeyHint> {
let mut h = vec![HINT_HELP, HINT_BKM_CREATE];
match ctx.selected_bookmark_kind {
Some(BookmarkKind::LocalJumpable) => {
h.push(HINT_JUMP_ENTER);
h.push(HINT_DEL_BKM);
h.push(HINT_RENAME);
h.push(HINT_FORGET);
h.push(HINT_MOVE_BKM);
}
Some(BookmarkKind::LocalNoChange) => {
h.push(HINT_DEL_BKM);
h.push(HINT_RENAME);
h.push(HINT_FORGET);
h.push(HINT_MOVE_BKM);
}
Some(BookmarkKind::TrackedRemote) => {
h.push(HINT_UNTRACK);
}
Some(BookmarkKind::UntrackedRemote) => {
h.push(HINT_TRACK);
}
None => {}
}
h.extend(undo_redo_refresh_back());
h
}
fn tag_view_hints() -> Vec<KeyHint> {
let mut h = vec![
HINT_HELP,
HINT_NAV,
HINT_JUMP_ENTER,
HINT_TAG_CREATE,
HINT_TAG_DELETE,
];
h.extend(undo_redo_refresh_back());
h
}
fn workspace_view_hints() -> Vec<KeyHint> {
let mut h = vec![
HINT_HELP,
HINT_NAV,
HINT_JUMP_ENTER,
KeyHint {
key: "n",
label: "Add",
color: Color::Green,
},
KeyHint {
key: "d",
label: "Forget",
color: Color::Red,
},
HINT_RENAME,
];
h.extend(undo_redo_refresh_back());
h
}
fn command_history_hints() -> Vec<KeyHint> {
vec![HINT_HELP, HINT_NAV, HINT_DETAIL, HINT_BACK]
}
pub const PARALLELIZE_SELECT_HINTS: &[KeyHint] = &[
KeyHint {
key: "j/k",
label: "Navigate",
color: Color::Blue,
},
KeyHint {
key: "Enter",
label: "Parallelize",
color: Color::Green,
},
KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
},
];
pub const COMPARE_SELECT_HINTS: &[KeyHint] = &[
KeyHint {
key: "j/k",
label: "Navigate",
color: Color::Blue,
},
KeyHint {
key: "Enter",
label: "Compare",
color: Color::Green,
},
KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
},
];
pub const INTERDIFF_SELECT_HINTS: &[KeyHint] = &[
KeyHint {
key: "j/k",
label: "Navigate",
color: Color::Blue,
},
KeyHint {
key: "Enter",
label: "Interdiff",
color: Color::Green,
},
KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
},
];
pub const BISECT_SELECT_HINTS: &[KeyHint] = &[
KeyHint {
key: "j/k",
label: "Navigate",
color: Color::Blue,
},
KeyHint {
key: "Enter",
label: "Select Good",
color: Color::Green,
},
KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
},
];
pub const REBASE_MODE_SELECT_HINTS: &[KeyHint] = &[
KeyHint {
key: "r",
label: "Revision",
color: Color::Yellow,
},
KeyHint {
key: "s",
label: "Source",
color: Color::Magenta,
},
KeyHint {
key: "b",
label: "Branch",
color: Color::LightBlue,
},
KeyHint {
key: "A",
label: "After",
color: Color::Cyan,
},
KeyHint {
key: "B",
label: "Before",
color: Color::Green,
},
KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
},
];
fn rebase_select_hints(
skip_emptied: bool,
simplify_parents: bool,
rebase_mode: crate::model::RebaseMode,
) -> Vec<KeyHint> {
use crate::model::RebaseMode;
let skip_label = if skip_emptied {
"Skip-empty:ON"
} else {
"Skip-empty:OFF"
};
let skip_color = if skip_emptied {
Color::Green
} else {
Color::DarkGray
};
let simplify_label = if simplify_parents {
"Simplify:ON"
} else {
"Simplify:OFF"
};
let simplify_color = if simplify_parents {
Color::Green
} else {
Color::DarkGray
};
let mut hints = vec![
KeyHint {
key: "j/k",
label: "Navigate",
color: Color::Blue,
},
KeyHint {
key: "S",
label: skip_label,
color: skip_color,
},
KeyHint {
key: "P",
label: simplify_label,
color: simplify_color,
},
];
if matches!(
rebase_mode,
RebaseMode::Revision | RebaseMode::Source | RebaseMode::Branch
) {
hints.push(KeyHint {
key: ":",
label: "Revset",
color: Color::Magenta,
});
}
hints.push(KeyHint {
key: "Enter",
label: "Rebase",
color: Color::Green,
});
hints.push(KeyHint {
key: "Esc",
label: "Cancel",
color: Color::Red,
});
hints
}
pub const DIFF_VIEW_HINTS: &[KeyHint] = &[
HINT_HELP,
KeyHint {
key: "j/k",
label: "Scroll",
color: Color::Cyan,
},
KeyHint {
key: "m",
label: "Mode",
color: Color::Green,
},
KeyHint {
key: "[/]",
label: "Prev/next file",
color: Color::Magenta,
},
KeyHint {
key: "a",
label: "Blame",
color: Color::Magenta,
},
KeyHint {
key: "y/Y",
label: "Copy",
color: Color::Yellow,
},
KeyHint {
key: "w",
label: "Export",
color: Color::Yellow,
},
KeyHint {
key: "^L",
label: "Refresh",
color: Color::Blue,
},
KeyHint {
key: "q",
label: "Back",
color: Color::Red,
},
];
pub const STATUS_VIEW_HINTS: &[KeyHint] = &[
KeyHint {
key: "?",
label: "Help",
color: Color::Cyan,
},
KeyHint {
key: "Enter",
label: "Diff",
color: Color::Green,
},
KeyHint {
key: "a",
label: "Blame",
color: Color::Magenta,
},
KeyHint {
key: "C",
label: "Commit",
color: Color::Yellow,
},
KeyHint {
key: "f",
label: "Conflict",
color: Color::Red,
},
KeyHint {
key: "r",
label: "Restore",
color: Color::Red,
},
KeyHint {
key: "R",
label: "RestoreAll",
color: Color::Red,
},
KeyHint {
key: "E",
label: "DiffEdit",
color: Color::Yellow,
},
HINT_UNDO,
HINT_REDO,
KeyHint {
key: "^L",
label: "Refresh",
color: Color::Blue,
},
KeyHint {
key: "Tab",
label: "Switch",
color: Color::Blue,
},
KeyHint {
key: "q",
label: "Back",
color: Color::Red,
},
];
pub const STATUS_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Show file diff",
},
KeyBindEntry {
key: "a",
description: "Show file blame",
},
KeyBindEntry {
key: "C",
description: "Commit changes",
},
KeyBindEntry {
key: "f",
description: "Jump to conflict",
},
KeyBindEntry {
key: "r",
description: "Restore file",
},
KeyBindEntry {
key: "R",
description: "Restore all files",
},
KeyBindEntry {
key: "E",
description: "Diffedit (external diff editor)",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "Tab",
description: "Switch to log",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
pub const OPERATION_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Restore operation",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
pub const BOOKMARK_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Jump to bookmark in log",
},
KeyBindEntry {
key: "n",
description: "Create bookmark",
},
KeyBindEntry {
key: "d",
description: "Delete local bookmark",
},
KeyBindEntry {
key: "t",
description: "Track remote bookmark",
},
KeyBindEntry {
key: "T",
description: "Untrack remote bookmark",
},
KeyBindEntry {
key: "r",
description: "Rename bookmark",
},
KeyBindEntry {
key: "f",
description: "Forget bookmark (remove tracking)",
},
KeyBindEntry {
key: "m",
description: "Move bookmark to @",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
pub const TAG_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Jump to tag in log",
},
KeyBindEntry {
key: "n",
description: "Create tag on @",
},
KeyBindEntry {
key: "d",
description: "Delete tag",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
pub const WORKSPACE_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Jump to working copy in log",
},
KeyBindEntry {
key: "n",
description: "Add workspace",
},
KeyBindEntry {
key: "d",
description: "Forget workspace",
},
KeyBindEntry {
key: "r",
description: "Rename workspace (current only)",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
pub const COMMAND_HISTORY_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Toggle detail",
},
KeyBindEntry {
key: "q",
description: "Back",
},
];
#[allow(dead_code)] pub const BLAME_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "g/G",
description: "Go to top/bottom",
},
KeyBindEntry {
key: "Enter",
description: "Show diff",
},
KeyBindEntry {
key: "J",
description: "Jump to change in log",
},
KeyBindEntry {
key: "q",
description: "Back",
},
];
pub const EVOLOG_KEYS: &[KeyBindEntry] = &[KeyBindEntry {
key: "Enter",
description: "Show this revision's diff",
}];
pub const HELP_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "/",
description: "Search help",
},
KeyBindEntry {
key: "n/N",
description: "Next/prev match",
},
KeyBindEntry {
key: "a",
description: "Toggle all views / current view",
},
KeyBindEntry {
key: "q/Esc",
description: "Close help",
},
];
#[allow(dead_code)] pub const RESOLVE_KEYS: &[KeyBindEntry] = &[
KeyBindEntry {
key: "j/k",
description: "Move down/up",
},
KeyBindEntry {
key: "Enter",
description: "Resolve (external tool, @ only)",
},
KeyBindEntry {
key: "o",
description: "Resolve with :ours",
},
KeyBindEntry {
key: "t",
description: "Resolve with :theirs",
},
KeyBindEntry {
key: "d",
description: "Show diff",
},
KeyBindEntry {
key: "u",
description: "Undo",
},
KeyBindEntry {
key: "Ctrl+r",
description: "Redo",
},
KeyBindEntry {
key: "q",
description: "Back to log",
},
];
pub const HELP_TOGGLE_ALL: KeyCode = KeyCode::Char('a');
pub fn keys_for_view(view: View) -> &'static [KeyBindEntry] {
match view {
View::Log => LOG_KEYS,
View::Status => STATUS_KEYS,
View::Diff => DIFF_KEYS,
View::Operation => OPERATION_KEYS,
View::Bookmark => BOOKMARK_KEYS,
View::Tag => TAG_KEYS,
View::Workspace => WORKSPACE_KEYS,
View::CommandHistory => COMMAND_HISTORY_KEYS,
View::Blame => BLAME_KEYS,
View::Resolve => RESOLVE_KEYS,
View::Evolog => EVOLOG_KEYS,
View::Help => HELP_KEYS,
}
}
pub const OPERATION_VIEW_HINTS: &[KeyHint] = &[
HINT_HELP,
HINT_NAV,
KeyHint {
key: "Enter",
label: "Restore",
color: Color::Green,
},
HINT_UNDO,
HINT_REDO,
KeyHint {
key: "^L",
label: "Refresh",
color: Color::Blue,
},
KeyHint {
key: "q",
label: "Back",
color: Color::Red,
},
];
pub const BLAME_VIEW_HINTS: &[KeyHint] = &[
HINT_HELP,
HINT_NAV,
KeyHint {
key: "Enter",
label: "Diff",
color: Color::Green,
},
KeyHint {
key: "J",
label: "Log Jump",
color: Color::Yellow,
},
KeyHint {
key: "^L",
label: "Refresh",
color: Color::Blue,
},
KeyHint {
key: "q",
label: "Back",
color: Color::Red,
},
];
pub const EVOLOG_VIEW_HINTS: &[KeyHint] = &[
HINT_HELP,
HINT_NAV,
KeyHint {
key: "Enter",
label: "Diff",
color: Color::Cyan,
},
HINT_REFRESH,
HINT_BACK,
];
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn object_view_verbs_are_normalized() {
assert_eq!(OBJECT_NEW, KeyCode::Char('n'), "create/add = n");
assert_eq!(OBJECT_DELETE, KeyCode::Char('d'), "delete/forget = d");
assert_eq!(TRACK, KeyCode::Char('t'), "track = t");
assert_eq!(
BOOKMARK_UNTRACK,
KeyCode::Char('T'),
"untrack = T (shift inverse)"
);
assert_eq!(OBJECT_RENAME, KeyCode::Char('r'), "rename = r");
assert_eq!(BOOKMARK_FORGET, KeyCode::Char('f'), "forget(tracking) = f");
assert_eq!(BOOKMARK_MOVE, KeyCode::Char('m'), "move = m");
}
#[test]
fn bookmark_view_opener_is_single_key_b() {
assert_eq!(BOOKMARK_VIEW, KeyCode::Char('b'), "b opens Bookmark View");
}
#[test]
fn undo_hint_in_all_mutating_views() {
let ctx = HintContext::default();
let has_undo = |v| {
current_hints(v, InputMode::Normal, &ctx)
.iter()
.any(|h| h.key == "u")
};
for v in [
View::Log,
View::Status,
View::Bookmark,
View::Tag,
View::Workspace,
View::Resolve,
] {
assert!(has_undo(v), "{v:?} (mutating) must show undo");
}
assert!(
!has_undo(View::CommandHistory),
"CommandHistory must NOT show undo (read-only)"
);
}
#[test]
fn delete_forget_terminology_matches_function() {
assert!(
TAG_KEYS.iter().any(|e| e.description.contains("Delete")),
"tag uses Delete"
);
assert!(
WORKSPACE_KEYS
.iter()
.any(|e| e.description.contains("Forget")),
"workspace uses Forget"
);
}
#[test]
fn bookmark_delete_status_hint_uses_delete_label() {
for kind in [BookmarkKind::LocalJumpable, BookmarkKind::LocalNoChange] {
let ctx = HintContext {
selected_bookmark_kind: Some(kind),
..HintContext::default()
};
let hints = current_hints(View::Bookmark, InputMode::Normal, &ctx);
let d = hints
.iter()
.find(|h| h.key == "d")
.unwrap_or_else(|| panic!("{kind:?} should expose d (delete) hint"));
assert_eq!(d.label, "Delete", "{kind:?} delete hint label");
}
}
#[test]
fn resolve_and_evolog_have_refresh_hint() {
let ctx = HintContext::default();
let resolve = current_hints(View::Resolve, InputMode::Normal, &ctx);
assert!(
resolve.iter().any(|h| h.key == "^L"),
"Resolve needs ^L refresh"
);
assert!(
EVOLOG_VIEW_HINTS.iter().any(|h| h.key == "^L"),
"Evolog needs ^L refresh"
);
}
#[test]
fn undo_redo_hints_match_mutating_views() {
let ctx = HintContext::default();
let has_cur = |v, k| {
current_hints(v, InputMode::Normal, &ctx)
.iter()
.any(|h| h.key == k)
};
for v in [
View::Log,
View::Status,
View::Bookmark,
View::Tag,
View::Workspace,
View::Resolve,
] {
assert!(has_cur(v, "u"), "{v:?} must show undo hint");
assert!(has_cur(v, "^R"), "{v:?} must show redo hint");
}
let ch = View::CommandHistory;
assert!(!has_cur(ch, "u"), "{ch:?} must not show undo hint");
assert!(!has_cur(ch, "^R"), "{ch:?} must not show redo hint");
assert!(
OPERATION_VIEW_HINTS.iter().any(|h| h.key == "u"),
"Operation must show undo hint"
);
assert!(
OPERATION_VIEW_HINTS.iter().any(|h| h.key == "^R"),
"Operation must show redo hint"
);
assert!(
!BLAME_VIEW_HINTS
.iter()
.any(|h| h.key == "u" || h.key == "^R"),
"Blame must not show undo/redo hints"
);
assert!(
!EVOLOG_VIEW_HINTS
.iter()
.any(|h| h.key == "u" || h.key == "^R"),
"Evolog must not show undo/redo hints"
);
assert!(
!DIFF_VIEW_HINTS
.iter()
.any(|h| h.key == "u" || h.key == "^R"),
"Diff must not show undo/redo hints"
);
}
#[test]
fn command_history_has_no_refresh_hint() {
let ctx = HintContext::default();
let ch = current_hints(View::CommandHistory, InputMode::Normal, &ctx);
assert!(
!ch.iter().any(|h| h.key == "^L"),
"CommandHistory must not show refresh (no-op)"
);
}
#[test]
fn operation_and_blame_nav_label_is_navigate() {
let op = OPERATION_VIEW_HINTS
.iter()
.find(|h| h.key == "j/k")
.expect("op has j/k");
assert_eq!(op.label, "Navigate", "Operation nav label");
let bl = BLAME_VIEW_HINTS
.iter()
.find(|h| h.key == "j/k")
.expect("blame has j/k");
assert_eq!(bl.label, "Navigate", "Blame nav label");
}
#[test]
fn all_status_bar_views_include_help_hint() {
let ctx = HintContext::default();
for view in [
View::Log,
View::Status,
View::Operation,
View::Resolve,
View::Bookmark,
View::Tag,
View::Workspace,
View::CommandHistory,
] {
let hints = current_hints(view, InputMode::Normal, &ctx);
assert!(
hints.iter().any(|h| h.key == "?"),
"{view:?} status bar must include the ? help hint"
);
}
}
#[test]
fn diff_and_blame_hints_include_help() {
assert!(
DIFF_VIEW_HINTS.iter().any(|h| h.key == "?"),
"Diff status bar needs ? help"
);
assert!(
BLAME_VIEW_HINTS.iter().any(|h| h.key == "?"),
"Blame status bar needs ? help"
);
}
#[test]
fn help_hint_appears_exactly_once_per_view() {
let ctx = HintContext::default();
for view in [
View::Log,
View::Status,
View::Operation,
View::Resolve,
View::Bookmark,
View::Tag,
View::Workspace,
View::CommandHistory,
] {
let hints = current_hints(view, InputMode::Normal, &ctx);
let n = hints.iter().filter(|h| h.key == "?").count();
assert_eq!(n, 1, "{view:?} must have exactly one ? hint, got {n}");
}
assert_eq!(DIFF_VIEW_HINTS.iter().filter(|h| h.key == "?").count(), 1);
assert_eq!(BLAME_VIEW_HINTS.iter().filter(|h| h.key == "?").count(), 1);
}
#[test]
fn log_normal_with_bookmarks_includes_push() {
let ctx = HintContext {
has_bookmarks: true,
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert!(hints.iter().any(|h| h.key == "P"), "Push hint missing");
assert!(
!hints.iter().any(|h| h.key == "D"),
"Del Bkm should not be a single-key hint in Log Normal"
);
assert!(
hints.iter().any(|h| h.key == "b"),
"bookmark view opener hint missing"
);
}
#[test]
fn log_normal_without_bookmarks_excludes_push_and_del() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert!(
!hints.iter().any(|h| h.key == "P"),
"Push hint should not appear"
);
assert!(
!hints.iter().any(|h| h.key == "D"),
"Del Bkm hint should not appear"
);
}
#[test]
fn log_normal_with_conflicts_includes_resolve() {
let ctx = HintContext {
has_conflicts: true,
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert!(hints.iter().any(|h| h.key == "X"), "Resolve hint missing");
}
#[test]
fn log_normal_without_conflicts_excludes_resolve() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert!(
!hints.iter().any(|h| h.key == "X"),
"Resolve hint should not appear"
);
}
#[test]
fn log_normal_always_includes_core_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert!(hints.iter().any(|h| h.key == "?"), "Help hint missing");
assert!(hints.iter().any(|h| h.key == "d"), "Desc hint missing");
assert!(hints.iter().any(|h| h.key == "q"), "Quit hint missing");
assert!(hints.iter().any(|h| h.key == "e"), "Edit hint missing");
assert!(hints.iter().any(|h| h.key == "c"), "New hint missing");
assert!(hints.iter().any(|h| h.key == "S"), "Squash hint missing");
assert!(hints.iter().any(|h| h.key == "F"), "Fetch hint missing");
}
#[test]
fn log_squash_select_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::SquashSelect, &ctx);
assert_eq!(hints.len(), 3);
assert!(hints.iter().any(|h| h.label == "Navigate"));
assert!(hints.iter().any(|h| h.label == "Squash"));
assert!(hints.iter().any(|h| h.label == "Cancel"));
}
#[test]
fn log_search_input_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::SearchInput, &ctx);
assert_eq!(hints.len(), 2);
}
#[test]
fn log_revset_input_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::RevsetInput, &ctx);
assert_eq!(hints.len(), 2);
}
#[test]
fn log_describe_input_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::DescribeInput, &ctx);
assert_eq!(hints.len(), 2);
}
#[test]
fn log_bookmark_input_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::BookmarkInput, &ctx);
assert_eq!(hints.len(), 2);
}
#[test]
fn dialog_confirm_hints() {
let ctx = HintContext {
dialog: Some(DialogHintKind::Confirm),
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert_eq!(hints.len(), 2);
assert!(hints.iter().any(|h| h.label == "Yes"));
assert!(hints.iter().any(|h| h.label == "No"));
}
#[test]
fn dialog_select_hints() {
let ctx = HintContext {
dialog: Some(DialogHintKind::Select),
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert_eq!(hints.len(), 4);
assert!(hints.iter().any(|h| h.label == "Navigate"));
assert!(hints.iter().any(|h| h.label == "Toggle"));
assert!(hints.iter().any(|h| h.label == "Confirm"));
assert!(hints.iter().any(|h| h.label == "Cancel"));
}
#[test]
fn dialog_single_select_hints() {
let ctx = HintContext {
dialog: Some(DialogHintKind::SingleSelect),
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert_eq!(hints.len(), 3);
assert!(hints.iter().any(|h| h.label == "Navigate"));
assert!(hints.iter().any(|h| h.label == "Select"));
assert!(hints.iter().any(|h| h.label == "Cancel"));
}
#[test]
fn dialog_overrides_log_normal() {
let ctx = HintContext {
has_bookmarks: true,
dialog: Some(DialogHintKind::Confirm),
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert_eq!(hints.len(), 2);
assert!(hints.iter().any(|h| h.label == "Yes"));
assert!(
!hints.iter().any(|h| h.key == "P"),
"Log hints should be suppressed"
);
}
#[test]
fn dialog_overrides_squash_select() {
let ctx = HintContext {
dialog: Some(DialogHintKind::Select),
..HintContext::default()
};
let hints = current_hints(View::Log, InputMode::SquashSelect, &ctx);
assert_eq!(hints.len(), 4);
assert!(hints.iter().any(|h| h.label == "Toggle"));
}
#[test]
fn dialog_overrides_status_view() {
let ctx = HintContext {
dialog: Some(DialogHintKind::Confirm),
..HintContext::default()
};
let hints = current_hints(View::Status, InputMode::Normal, &ctx);
assert_eq!(hints.len(), 2);
assert!(hints.iter().any(|h| h.label == "Yes"));
}
#[test]
fn no_dialog_returns_view_hints() {
let ctx = HintContext::default();
let hints = current_hints(View::Log, InputMode::Normal, &ctx);
assert!(hints.len() > 10, "Should return full Log Normal hints");
}
#[test]
fn resolve_working_copy_includes_enter() {
let ctx = HintContext {
is_working_copy: true,
..HintContext::default()
};
let hints = current_hints(View::Resolve, InputMode::Normal, &ctx);
assert!(
hints
.iter()
.any(|h| h.key == "Enter" && h.label == "Resolve"),
"Resolve Enter hint missing for working copy"
);
}
#[test]
fn resolve_non_working_copy_excludes_enter() {
let ctx = HintContext::default();
let hints = current_hints(View::Resolve, InputMode::Normal, &ctx);
assert!(
!hints.iter().any(|h| h.key == "Enter"),
"Enter hint should not appear for non-working-copy"
);
}
#[test]
fn resolve_always_includes_ours_theirs_diff_back() {
let ctx = HintContext::default();
let hints = current_hints(View::Resolve, InputMode::Normal, &ctx);
assert!(hints.iter().any(|h| h.key == "o"), "Ours hint missing");
assert!(hints.iter().any(|h| h.key == "t"), "Theirs hint missing");
assert!(hints.iter().any(|h| h.key == "d"), "Diff hint missing");
assert!(hints.iter().any(|h| h.key == "q"), "Back hint missing");
}
#[test]
fn diff_view_returns_empty() {
let ctx = HintContext::default();
let hints = current_hints(View::Diff, InputMode::Normal, &ctx);
assert!(hints.is_empty());
}
#[test]
fn help_view_returns_empty() {
let ctx = HintContext::default();
let hints = current_hints(View::Help, InputMode::Normal, &ctx);
assert!(hints.is_empty());
}
#[test]
fn help_arrays_list_undo_redo_for_mutating_views() {
for v in [
View::Log,
View::Status,
View::Bookmark,
View::Tag,
View::Workspace,
View::Operation,
View::Resolve,
] {
let ks = keys_for_view(v);
assert!(
ks.iter().any(|e| e.description.contains("Undo")),
"{v:?} help missing Undo"
);
assert!(
ks.iter().any(|e| e.description.contains("Redo")),
"{v:?} help missing Redo"
);
}
for v in [
View::Diff,
View::Blame,
View::Evolog,
View::CommandHistory,
View::Help,
] {
let ks = keys_for_view(v);
assert!(
!ks.iter().any(|e| e.description.contains("Undo")),
"{v:?} help should not list Undo"
);
assert!(
!ks.iter().any(|e| e.description.contains("Redo")),
"{v:?} help should not list Redo"
);
}
}
}