config/
key_bindings.rs

1use git::Config;
2
3use crate::{errors::ConfigError, utils::get_input};
4
5fn map_single_ascii_to_lower(s: &str) -> String {
6	if s.is_ascii() && s.len() == 1 {
7		s.to_lowercase()
8	}
9	else {
10		String::from(s)
11	}
12}
13
14/// Represents the key binding configuration options.
15#[derive(Clone, Debug)]
16#[non_exhaustive]
17pub struct KeyBindings {
18	/// Key bindings for aborting.
19	pub abort: Vec<String>,
20	/// Key bindings for the break action.
21	pub action_break: Vec<String>,
22	/// Key bindings for the drop action.
23	pub action_drop: Vec<String>,
24	/// Key bindings for the edit action.
25	pub action_edit: Vec<String>,
26	/// Key bindings for the fixup action.
27	pub action_fixup: Vec<String>,
28	/// Key bindings for the pick action.
29	pub action_pick: Vec<String>,
30	/// Key bindings for the reword action.
31	pub action_reword: Vec<String>,
32	/// Key bindings for the squash action.
33	pub action_squash: Vec<String>,
34	/// Key bindings for negative confirmation.
35	pub confirm_no: Vec<String>,
36	/// Key bindings for positive confirmation.
37	pub confirm_yes: Vec<String>,
38	/// Key bindings for editing.
39	pub edit: Vec<String>,
40	/// Key bindings for forcing a abort.
41	pub force_abort: Vec<String>,
42	/// Key bindings for forcing a rebase.
43	pub force_rebase: Vec<String>,
44	/// Key bindings for showing help.
45	pub help: Vec<String>,
46	/// Key bindings for inserting a line.
47	pub insert_line: Vec<String>,
48
49	/// Key bindings for moving down.
50	pub move_down: Vec<String>,
51	/// Key bindings for moving to the end.
52	pub move_end: Vec<String>,
53	/// Key bindings for moving to the start.
54	pub move_home: Vec<String>,
55	/// Key bindings for moving to the left.
56	pub move_left: Vec<String>,
57	/// Key bindings for moving to the right.
58	pub move_right: Vec<String>,
59	/// Key bindings for moving up.
60	pub move_up: Vec<String>,
61	/// Key bindings for moving down a step.
62	pub move_down_step: Vec<String>,
63	/// Key bindings for moving up a step.
64	pub move_up_step: Vec<String>,
65	/// Key bindings for moving the selection down.
66	pub move_selection_down: Vec<String>,
67	/// Key bindings for moving the selection up.
68	pub move_selection_up: Vec<String>,
69
70	/// Key bindings for scrolling down.
71	pub scroll_down: Vec<String>,
72	/// Key bindings for scrolling to the end.
73	pub scroll_end: Vec<String>,
74	/// Key bindings for scrolling to the start.
75	pub scroll_home: Vec<String>,
76	/// Key bindings for scrolling to the left.
77	pub scroll_left: Vec<String>,
78	/// Key bindings for scrolling to the right.
79	pub scroll_right: Vec<String>,
80	/// Key bindings for scrolling up.
81	pub scroll_up: Vec<String>,
82	/// Key bindings for scrolling down a step.
83	pub scroll_step_down: Vec<String>,
84	/// Key bindings for scrolling up a step.
85	pub scroll_step_up: Vec<String>,
86
87	/// Key bindings for opening the external editor.
88	pub open_in_external_editor: Vec<String>,
89	/// Key bindings for rebasing.
90	pub rebase: Vec<String>,
91	/// Key bindings for redoing a change.
92	pub redo: Vec<String>,
93	/// Key bindings for removing a line.
94	pub remove_line: Vec<String>,
95	/// Key bindings for starting search.
96	pub search_start: Vec<String>,
97	/// Key bindings for next search match.
98	pub search_next: Vec<String>,
99	/// Key bindings for previous search match.
100	pub search_previous: Vec<String>,
101	/// Key bindings for showing a commit.
102	pub show_commit: Vec<String>,
103	/// Key bindings for showing a diff.
104	pub show_diff: Vec<String>,
105	/// Key bindings for toggling visual mode.
106	pub toggle_visual_mode: Vec<String>,
107	/// Key bindings for undoing a change.
108	pub undo: Vec<String>,
109	/// Key bindings for the fixup specific action to toggle the c option.
110	pub fixup_keep_message_with_editor: Vec<String>,
111	/// Key bindings for the fixup specific action to toggle the c option.
112	pub fixup_keep_message: Vec<String>,
113}
114
115impl KeyBindings {
116	/// Create a new configuration with default values.
117	#[must_use]
118	#[inline]
119	#[allow(clippy::missing_panics_doc)]
120	pub fn new() -> Self {
121		Self::new_with_config(None).unwrap() // should never error with None config
122	}
123
124	pub(super) fn new_with_config(git_config: Option<&Config>) -> Result<Self, ConfigError> {
125		let confirm_no = get_input(git_config, "interactive-rebase-tool.inputConfirmNo", "n")?
126			.iter()
127			.map(|s| map_single_ascii_to_lower(s))
128			.collect();
129		let confirm_yes = get_input(git_config, "interactive-rebase-tool.inputConfirmYes", "y")?
130			.iter()
131			.map(|s| map_single_ascii_to_lower(s))
132			.collect();
133		Ok(Self {
134			abort: get_input(git_config, "interactive-rebase-tool.inputAbort", "q")?,
135			action_break: get_input(git_config, "interactive-rebase-tool.inputActionBreak", "b")?,
136			action_drop: get_input(git_config, "interactive-rebase-tool.inputActionDrop", "d")?,
137			action_edit: get_input(git_config, "interactive-rebase-tool.inputActionEdit", "e")?,
138			action_fixup: get_input(git_config, "interactive-rebase-tool.inputActionFixup", "f")?,
139			action_pick: get_input(git_config, "interactive-rebase-tool.inputActionPick", "p")?,
140			action_reword: get_input(git_config, "interactive-rebase-tool.inputActionReword", "r")?,
141			action_squash: get_input(git_config, "interactive-rebase-tool.inputActionSquash", "s")?,
142			confirm_no,
143			confirm_yes,
144			edit: get_input(git_config, "interactive-rebase-tool.inputEdit", "E")?,
145			force_abort: get_input(git_config, "interactive-rebase-tool.inputForceAbort", "Q")?,
146			force_rebase: get_input(git_config, "interactive-rebase-tool.inputForceRebase", "W")?,
147			help: get_input(git_config, "interactive-rebase-tool.inputHelp", "?")?,
148			insert_line: get_input(git_config, "interactive-rebase-tool.insertLine", "I")?,
149			move_down: get_input(git_config, "interactive-rebase-tool.inputMoveDown", "Down")?,
150			move_end: get_input(git_config, "interactive-rebase-tool.inputMoveEnd", "End")?,
151			move_home: get_input(git_config, "interactive-rebase-tool.inputMoveHome", "Home")?,
152			move_left: get_input(git_config, "interactive-rebase-tool.inputMoveLeft", "Left")?,
153			move_right: get_input(git_config, "interactive-rebase-tool.inputMoveRight", "Right")?,
154			move_down_step: get_input(git_config, "interactive-rebase-tool.inputMoveStepDown", "PageDown")?,
155			move_up_step: get_input(git_config, "interactive-rebase-tool.inputMoveStepUp", "PageUp")?,
156			move_up: get_input(git_config, "interactive-rebase-tool.inputMoveUp", "Up")?,
157			move_selection_down: get_input(git_config, "interactive-rebase-tool.inputMoveSelectionDown", "j")?,
158			move_selection_up: get_input(git_config, "interactive-rebase-tool.inputMoveSelectionUp", "k")?,
159			scroll_down: get_input(git_config, "interactive-rebase-tool.inputScrollDown", "Down")?,
160			scroll_end: get_input(git_config, "interactive-rebase-tool.inputScrollEnd", "End")?,
161			scroll_home: get_input(git_config, "interactive-rebase-tool.inputScrollHome", "Home")?,
162			scroll_left: get_input(git_config, "interactive-rebase-tool.inputScrollLeft", "Left")?,
163			scroll_right: get_input(git_config, "interactive-rebase-tool.inputScrollRight", "Right")?,
164			scroll_up: get_input(git_config, "interactive-rebase-tool.inputScrollUp", "Up")?,
165			scroll_step_down: get_input(git_config, "interactive-rebase-tool.inputScrollStepDown", "PageDown")?,
166			scroll_step_up: get_input(git_config, "interactive-rebase-tool.inputScrollStepUp", "PageUp")?,
167			open_in_external_editor: get_input(git_config, "interactive-rebase-tool.inputOpenInExternalEditor", "!")?,
168			rebase: get_input(git_config, "interactive-rebase-tool.inputRebase", "w")?,
169			redo: get_input(git_config, "interactive-rebase-tool.inputRedo", "control+y")?,
170			remove_line: get_input(git_config, "interactive-rebase-tool.removeLine", "delete")?,
171			search_start: get_input(git_config, "interactive-rebase-tool.searchStart", "/")?,
172			search_next: get_input(git_config, "interactive-rebase-tool.searchNext", "n")?,
173			search_previous: get_input(git_config, "interactive-rebase-tool.searchPrevious", "N")?,
174			show_commit: get_input(git_config, "interactive-rebase-tool.inputShowCommit", "c")?,
175			show_diff: get_input(git_config, "interactive-rebase-tool.inputShowDiff", "d")?,
176			toggle_visual_mode: get_input(git_config, "interactive-rebase-tool.inputToggleVisualMode", "v")?,
177			undo: get_input(git_config, "interactive-rebase-tool.inputUndo", "control+z")?,
178			fixup_keep_message_with_editor: get_input(
179				git_config,
180				"interactive-rebase-tool.fixupKeepMessageWithEditor",
181				"U",
182			)?,
183			fixup_keep_message: get_input(git_config, "interactive-rebase-tool.fixupKeepMessage", "u")?,
184		})
185	}
186}
187
188impl TryFrom<&Config> for KeyBindings {
189	type Error = ConfigError;
190
191	#[inline]
192	fn try_from(config: &Config) -> Result<Self, Self::Error> {
193		Self::new_with_config(Some(config))
194	}
195}
196
197#[cfg(test)]
198mod tests {
199	use claim::assert_ok;
200
201	use super::*;
202	use crate::testutils::with_git_config;
203
204	macro_rules! config_test {
205		($key:ident, $config_name:literal, $default:literal) => {
206			let config = KeyBindings::new();
207			let value = config.$key[0].as_str();
208			assert_eq!(
209				value,
210				String::from($default),
211				"Default value for key binding '{}' was expected to be '{}' but '{}' was found",
212				stringify!($key),
213				$default,
214				value
215			);
216
217			let config_value = format!("{} = \"f255\"", $config_name);
218			with_git_config(
219				&["[interactive-rebase-tool]", config_value.as_str()],
220				|git_config| {
221					let config = KeyBindings::new_with_config(Some(&git_config)).unwrap();
222					assert_eq!(
223						config.$key[0].as_str(),
224						"F255",
225						"Value for key binding '{}' was expected to be changed but was not",
226						stringify!($key)
227					);
228				},
229			);
230		};
231	}
232
233	#[test]
234	fn new() {
235		let _config = KeyBindings::new();
236	}
237
238	#[test]
239	fn try_from_git_config() {
240		with_git_config(&[], |git_config| {
241			assert_ok!(KeyBindings::try_from(&git_config));
242		});
243	}
244
245	#[test]
246	fn try_from_git_config_error() {
247		with_git_config(&["[interactive-rebase-tool]", "inputAbort = invalid"], |git_config| {
248			_ = KeyBindings::try_from(&git_config).unwrap_err();
249		});
250	}
251
252	#[test]
253	fn key_bindings() {
254		config_test!(abort, "inputAbort", "q");
255		config_test!(action_break, "inputActionBreak", "b");
256		config_test!(action_drop, "inputActionDrop", "d");
257		config_test!(action_edit, "inputActionEdit", "e");
258		config_test!(action_fixup, "inputActionFixup", "f");
259		config_test!(action_pick, "inputActionPick", "p");
260		config_test!(action_reword, "inputActionReword", "r");
261		config_test!(action_squash, "inputActionSquash", "s");
262		config_test!(confirm_no, "inputConfirmNo", "n");
263		config_test!(confirm_yes, "inputConfirmYes", "y");
264		config_test!(edit, "inputEdit", "E");
265		config_test!(force_abort, "inputForceAbort", "Q");
266		config_test!(force_rebase, "inputForceRebase", "W");
267		config_test!(help, "inputHelp", "?");
268		config_test!(insert_line, "insertLine", "I");
269		config_test!(move_down, "inputMoveDown", "Down");
270		config_test!(move_end, "inputMoveEnd", "End");
271		config_test!(move_home, "inputMoveHome", "Home");
272		config_test!(move_left, "inputMoveLeft", "Left");
273		config_test!(move_right, "inputMoveRight", "Right");
274		config_test!(move_up, "inputMoveUp", "Up");
275		config_test!(move_down_step, "inputMoveStepDown", "PageDown");
276		config_test!(move_up_step, "inputMoveStepUp", "PageUp");
277		config_test!(move_selection_down, "inputMoveSelectionDown", "j");
278		config_test!(move_selection_up, "inputMoveSelectionUp", "k");
279		config_test!(scroll_down, "inputScrollDown", "Down");
280		config_test!(scroll_end, "inputScrollEnd", "End");
281		config_test!(scroll_home, "inputScrollHome", "Home");
282		config_test!(scroll_left, "inputScrollLeft", "Left");
283		config_test!(scroll_right, "inputScrollRight", "Right");
284		config_test!(scroll_up, "inputScrollUp", "Up");
285		config_test!(scroll_step_down, "inputScrollStepDown", "PageDown");
286		config_test!(scroll_step_up, "inputScrollStepUp", "PageUp");
287		config_test!(open_in_external_editor, "inputOpenInExternalEditor", "!");
288		config_test!(rebase, "inputRebase", "w");
289		config_test!(redo, "inputRedo", "Controly");
290		config_test!(remove_line, "removeLine", "Delete");
291		config_test!(search_start, "searchStart", "/");
292		config_test!(search_next, "searchNext", "n");
293		config_test!(search_previous, "searchPrevious", "N");
294		config_test!(show_commit, "inputShowCommit", "c");
295		config_test!(show_diff, "inputShowDiff", "d");
296		config_test!(toggle_visual_mode, "inputToggleVisualMode", "v");
297		config_test!(undo, "inputUndo", "Controlz");
298		config_test!(fixup_keep_message_with_editor, "fixupKeepMessageWithEditor", "U");
299		config_test!(fixup_keep_message, "fixupKeepMessage", "u");
300	}
301}