fix: distinguish Ctrl+I from Tab in comparisons
This commit is contained in:
+12
-29
@@ -1224,9 +1224,8 @@ fn parse_key(key: &str) -> Result<Input, String> {
|
||||
}
|
||||
|
||||
fn control_key(ch: char) -> Input {
|
||||
// Terminals encode these controls as named keys.
|
||||
// Preserve distinguishable control keys; Ctrl+M retains its Enter alias.
|
||||
match ch.to_ascii_lowercase() {
|
||||
'i' => Input::Char('\t'),
|
||||
'm' => Input::Enter,
|
||||
ch => Input::Ctrl(ch),
|
||||
}
|
||||
@@ -1299,6 +1298,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
(Input::Ctrl('o'), JumpBack),
|
||||
(Input::Ctrl('b'), DiffModeToggle),
|
||||
(Input::Char('\t'), JumpForward),
|
||||
(Input::Ctrl('i'), JumpForward),
|
||||
(Input::Alt('h'), PaneFocusLeft),
|
||||
(Input::Alt('j'), PaneFocusDown),
|
||||
(Input::Alt('k'), PaneFocusUp),
|
||||
@@ -1403,6 +1403,7 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
|
||||
.insert(Input::Ctrl('n'), Binding::Action(LspCompletion));
|
||||
for (key, action) in [
|
||||
(Input::Char('\t'), SnippetNext),
|
||||
(Input::Ctrl('i'), SnippetNext),
|
||||
(Input::BackTab, SnippetPrevious),
|
||||
] {
|
||||
bindings
|
||||
@@ -5139,7 +5140,11 @@ impl Editor {
|
||||
| Action::Plugins
|
||||
| Action::Buffers
|
||||
)
|
||||
)
|
||||
) || (input != Input::Char('\t')
|
||||
&& matches!(
|
||||
binding,
|
||||
Binding::Action(Action::JumpBack | Action::JumpForward)
|
||||
))
|
||||
})
|
||||
{
|
||||
return self.handle_input(input);
|
||||
@@ -18172,7 +18177,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn jump_keys_honor_control_aliases_custom_bindings_and_normal_mode_eligibility() {
|
||||
fn jump_keys_honor_independent_tab_bindings_and_normal_mode_eligibility() {
|
||||
let directory = Directory::new();
|
||||
let path = directory.path("text.txt");
|
||||
fs::write(&path, "zero\none\ntwo").unwrap();
|
||||
@@ -18193,12 +18198,9 @@ mod tests {
|
||||
editor
|
||||
.bind("normal", "ctrl+i", Some("core.right".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.cursor().column, 1);
|
||||
editor
|
||||
.bind("normal", "tab", Some("core.jump_forward".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('I'));
|
||||
assert_eq!(editor.cursor().column, 1);
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.cursor(), Cursor { row: 1, column: 1 });
|
||||
for mode in ["insert", "visual", "command", "search"] {
|
||||
for action in ["core.jump_back", "core.jump_forward"] {
|
||||
@@ -20425,11 +20427,7 @@ mod tests {
|
||||
#[test]
|
||||
fn ctrl_j_is_distinct_from_enter_and_preserves_editor_defaults() {
|
||||
assert_eq!(parse_key("CTRL+J").unwrap(), Input::Ctrl('j'));
|
||||
for (key, input) in [
|
||||
("ctrl+m", Input::Enter),
|
||||
("ctrl+h", Input::Ctrl('h')),
|
||||
("ctrl+i", Input::Char('\t')),
|
||||
] {
|
||||
for (key, input) in [("ctrl+m", Input::Enter), ("ctrl+h", Input::Ctrl('h'))] {
|
||||
assert_eq!(parse_key(key).unwrap(), input);
|
||||
}
|
||||
let mut editor = editor();
|
||||
@@ -24537,12 +24535,6 @@ mod tests {
|
||||
editor
|
||||
.bind("normal", "ctrl+j", Some("core.insert".into()))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
palette_entry(&editor, "core.insert").bindings,
|
||||
vec![
|
||||
"Ctrl+h", "Ctrl+j", "Ctrl+r", "Enter", "I", "Space", "Tab", "界"
|
||||
]
|
||||
);
|
||||
assert!(
|
||||
palette_entry(&editor, "core.insert_first_nonblank")
|
||||
.bindings
|
||||
@@ -25817,15 +25809,6 @@ mod tests {
|
||||
editor.handle(Input::Char('+'));
|
||||
assert_eq!(editor.lines(), &["界", ""]);
|
||||
|
||||
editor.bind("insert", "CTRL+I", None).unwrap();
|
||||
editor.handle(Input::Char('\t'));
|
||||
assert_eq!(editor.lines(), &["界", ""]);
|
||||
editor
|
||||
.bind("insert", "TAB", Some("core.newline".into()))
|
||||
.unwrap();
|
||||
editor.handle(Input::Ctrl('i'));
|
||||
assert_eq!(editor.lines(), &["界", "", ""]);
|
||||
|
||||
editor
|
||||
.bind("insert", "shift+j", Some("core.normal".into()))
|
||||
.unwrap();
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@ Normal: i/a insert, I/A insert at first nonblank/end, o/O new line
|
||||
gf follows Markdown file links/reference links and heading fragments (no LSP)
|
||||
Normal/Visual: gt first line, gb last line, at first nonblank; Esc cancels g
|
||||
Normal/Visual: <line>g jumps to that one-based source line at first nonblank, clamped at EOF
|
||||
Ctrl+o older jump location; Ctrl+i/Tab newer (counts supported)
|
||||
Ctrl+o older jump location; Ctrl+i newer (Tab too outside comparisons; counts supported)
|
||||
_ splits above/below, | left/right; Option/Alt+h/j/k/l focuses neighboring panes
|
||||
Ctrl+Option/Alt+h/l previous/next tab; :tab.new opens current buffer in another tab
|
||||
Normal/Visual: prefix motions with counts (5j, 2w, 3n); Esc cancels count
|
||||
|
||||
+84
@@ -9,6 +9,10 @@ use std::{
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use crossterm::event::{
|
||||
KeyboardEnhancementFlags, PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags,
|
||||
};
|
||||
use crossterm::{
|
||||
cursor::Show,
|
||||
event::{
|
||||
@@ -85,6 +89,8 @@ impl crossterm::Command for ColorSchemeNotifications {
|
||||
|
||||
fn restore_terminal() {
|
||||
let _ = disable_raw_mode();
|
||||
#[cfg(unix)]
|
||||
let _ = execute!(io::stdout(), PopKeyboardEnhancementFlags);
|
||||
let _ = execute!(
|
||||
io::stdout(),
|
||||
EndSynchronizedUpdate,
|
||||
@@ -128,6 +134,14 @@ impl TerminalSession {
|
||||
.active = true;
|
||||
if !inherited_screen {
|
||||
execute!(io::stdout(), EnterAlternateScreen)?;
|
||||
// Restart inherits this screen and its keyboard stack entry.
|
||||
#[cfg(unix)]
|
||||
execute!(
|
||||
io::stdout(),
|
||||
PushKeyboardEnhancementFlags(
|
||||
KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
|
||||
)
|
||||
)?;
|
||||
}
|
||||
execute!(
|
||||
io::stdout(),
|
||||
@@ -1494,6 +1508,7 @@ fn translate(key: KeyEvent) -> Option<Input> {
|
||||
KeyCode::Down => Input::Down,
|
||||
KeyCode::Home => Input::Home,
|
||||
KeyCode::End => Input::End,
|
||||
KeyCode::Tab if key.modifiers.contains(KeyModifiers::CONTROL) => Input::Ctrl('i'),
|
||||
KeyCode::Tab => Input::Char('\t'),
|
||||
KeyCode::BackTab => Input::BackTab,
|
||||
_ => return None,
|
||||
@@ -16028,6 +16043,68 @@ end
|
||||
assert_eq!(viewports.syntax.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn diff_control_i_restores_jump_location_without_cycling_panes() {
|
||||
for unified in [false, true] {
|
||||
for focus in [Focus::Before, Focus::After] {
|
||||
for code in [KeyCode::Char('i'), KeyCode::Tab] {
|
||||
let mut editor = readonly_diff_editor(syntax_diff_file(
|
||||
"jump.txt",
|
||||
"old\nmiddle\ntarget",
|
||||
"new\nmiddle\ntarget",
|
||||
));
|
||||
if unified {
|
||||
editor.execute("diff.view.toggle");
|
||||
}
|
||||
for _ in 0..3 {
|
||||
if editor.diff().unwrap().focus == focus {
|
||||
break;
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
assert_eq!(editor.diff().unwrap().focus, focus);
|
||||
let buffer = editor.buffer_id();
|
||||
for ch in "/target".chars() {
|
||||
editor.handle(Input::Char(ch));
|
||||
}
|
||||
editor.handle(Input::Enter);
|
||||
assert_eq!(editor.cursor(), Cursor { row: 2, column: 0 });
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.cursor(), Cursor::default());
|
||||
editor.handle(translate(KeyEvent::new(code, KeyModifiers::CONTROL)).unwrap());
|
||||
assert_eq!(editor.cursor(), Cursor { row: 2, column: 0 });
|
||||
assert_eq!(editor.diff().unwrap().focus, focus);
|
||||
assert_eq!(editor.buffer_id(), buffer);
|
||||
assert!(!editor.dirty());
|
||||
editor.handle(
|
||||
translate(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)).unwrap(),
|
||||
);
|
||||
assert_ne!(editor.diff().unwrap().focus, focus);
|
||||
for _ in 0..3 {
|
||||
if editor.diff().unwrap().focus == Focus::Files {
|
||||
break;
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Files);
|
||||
editor.handle(Input::Ctrl('o'));
|
||||
assert_eq!(editor.cursor(), Cursor::default());
|
||||
assert_eq!(editor.diff().unwrap().focus, focus);
|
||||
for _ in 0..3 {
|
||||
if editor.diff().unwrap().focus == Focus::Files {
|
||||
break;
|
||||
}
|
||||
editor.handle(Input::Char('\t'));
|
||||
}
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Files);
|
||||
editor.handle(translate(KeyEvent::new(code, KeyModifiers::CONTROL)).unwrap());
|
||||
assert_eq!(editor.cursor(), Cursor { row: 2, column: 0 });
|
||||
assert_eq!(editor.diff().unwrap().focus, Focus::Files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translate_distinguishes_control_j_from_enter_and_normalizes_control_letters() {
|
||||
for (code, modifiers, expected) in [
|
||||
@@ -20815,6 +20892,13 @@ end
|
||||
entered,
|
||||
"{mode}: {before:?}"
|
||||
);
|
||||
for sequence in ["\x1b[>1u", "\x1b[<1u"] {
|
||||
assert_eq!(
|
||||
before.matches(sequence).count(),
|
||||
entered,
|
||||
"{mode}: {before:?}"
|
||||
);
|
||||
}
|
||||
for protocol in [1000, 1002, 1003, 1015, 1006] {
|
||||
assert_eq!(
|
||||
before.matches(&format!("\x1b[?{protocol}h")).count(),
|
||||
|
||||
Reference in New Issue
Block a user