feat: support macOS modified backspace deletion

This commit is contained in:
2026-10-12 17:36:43 +02:00
parent 5e6b602127
commit 2260817fc0
6 changed files with 522 additions and 60 deletions
+52 -4
View File
@@ -1,4 +1,4 @@
use crate::editor::Input;
use crate::editor::{BackspaceKind, Input};
use crate::files::{FilePicker, FilePresentation, FileView};
use std::{
collections::BTreeSet,
@@ -506,9 +506,11 @@ impl DiffState {
Input::Home => self.selected = 0,
Input::End => self.selected = self.visible.len().saturating_sub(1),
Input::Enter if !self.loading => self.choose(),
Input::Backspace => {
if let Some((byte, _)) = self.query.grapheme_indices(true).next_back() {
self.query.truncate(byte);
input if BackspaceKind::from_input(&input).is_some() => {
if BackspaceKind::from_input(&input)
.expect("guard checked backspace input")
.truncate(&mut self.query)
{
self.refilter();
}
}
@@ -2113,6 +2115,52 @@ mod tests {
assert_eq!(editor.diff().unwrap().selected, 1);
}
#[test]
fn selector_word_and_line_deletion_preserve_source_in_both_stages() {
for after in [false, true] {
let mut editor = editor();
editor
.apply(vec![Effect::Replace("source text".into())])
.unwrap();
let source = editor.context().text;
let revision = editor.text_revision();
let carets = editor.carets();
open(&mut editor);
if after {
editor.handle(Input::Enter);
}
let phase = if after { Phase::After } else { Phase::Before };
text(&mut editor, "FIX missing");
assert_eq!(editor.diff().unwrap().entries().len(), 1);
editor.handle(Input::AltBackspace);
assert_eq!(editor.diff().unwrap().query, "FIX ");
assert_eq!(editor.diff().unwrap().entries().len(), 2);
editor.handle(Input::SuperBackspace);
assert!(editor.diff().unwrap().query.is_empty());
assert_eq!(
editor
.diff()
.unwrap()
.entries()
.map(|entry| entry.label.as_str())
.collect::<Vec<_>>(),
[
"Working checkout",
"abcdef123",
"fedcba321",
"Load history from origin"
]
);
for input in [Input::Backspace, Input::AltBackspace, Input::SuperBackspace] {
editor.handle(input);
}
assert_eq!(editor.diff().unwrap().phase, phase);
assert_eq!(editor.context().text, source);
assert_eq!(editor.text_revision(), revision);
assert_eq!(editor.carets(), carets);
}
}
#[test]
fn loader_preserves_after_step_query_category_and_selection_then_compares() {
let mut editor = editor();
+346 -40
View File
@@ -34,6 +34,8 @@ pub enum Input {
Escape,
Enter,
Backspace,
AltBackspace,
SuperBackspace,
Delete,
BackTab,
Left,
@@ -44,6 +46,46 @@ pub enum Input {
End,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum BackspaceKind {
Grapheme,
Word,
Line,
}
impl BackspaceKind {
pub(crate) fn from_input(input: &Input) -> Option<Self> {
match input {
Input::Backspace => Some(Self::Grapheme),
Input::AltBackspace => Some(Self::Word),
Input::SuperBackspace => Some(Self::Line),
_ => None,
}
}
fn start(self, prefix: &str) -> usize {
match self {
Self::Grapheme => prefix
.grapheme_indices(true)
.next_back()
.map_or(0, |(byte, _)| byte),
Self::Word => prefix
.split_word_bound_indices()
.rev()
.find(|(_, word)| !word.chars().all(char::is_whitespace))
.map_or(0, |(byte, _)| byte),
Self::Line => prefix.rfind('\n').map_or(0, |byte| byte + 1),
}
}
pub(crate) fn truncate(self, text: &mut String) -> bool {
let start = self.start(text);
let changed = start != text.len();
text.truncate(start);
changed
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Mode {
Normal,
@@ -295,7 +337,7 @@ enum InsertStep {
Bulk(String),
Move(Action, usize),
Delete(Vec<bool>),
Backspace(Vec<bool>),
Backspace(Vec<bool>, BackspaceKind),
Completion {
primary_edit: usize,
edits: Vec<RelativeCompletionEdit>,
@@ -583,6 +625,16 @@ core_actions![
Action::Backspace,
&[Mode::Insert, Mode::Command, Mode::Search],
),
(
"core.backspace_word",
Action::BackspaceWord,
&[Mode::Insert, Mode::Command, Mode::Search],
),
(
"core.backspace_line",
Action::BackspaceLine,
&[Mode::Insert, Mode::Command, Mode::Search],
),
(
"core.submit",
Action::Submit,
@@ -785,10 +837,19 @@ impl Action {
| Self::BufferDelete
| Self::BufferForceDelete
| Self::LspCompletion
) || (self == Self::Backspace && mode == Mode::Insert)
) || (self.backspace_kind().is_some() && mode == Mode::Insert)
|| (self == Self::SnippetNext && mode == Mode::Insert)
}
fn backspace_kind(self) -> Option<BackspaceKind> {
match self {
Self::Backspace => Some(BackspaceKind::Grapheme),
Self::BackspaceWord => Some(BackspaceKind::Word),
Self::BackspaceLine => Some(BackspaceKind::Line),
_ => None,
}
}
fn named(mode: Mode, name: &str) -> Option<Self> {
CORE_ACTIONS
.iter()
@@ -871,6 +932,16 @@ fn parse_key(key: &str) -> Result<Input, String> {
(Some(modifier), Input::Char('\t')) if modifier.eq_ignore_ascii_case("shift") => {
Ok(Input::BackTab)
}
(Some(modifier), Input::Backspace)
if modifier.eq_ignore_ascii_case("alt") || modifier.eq_ignore_ascii_case("option") =>
{
Ok(Input::AltBackspace)
}
(Some(modifier), Input::Backspace)
if modifier.eq_ignore_ascii_case("cmd") || modifier.eq_ignore_ascii_case("super") =>
{
Ok(Input::SuperBackspace)
}
(Some(modifier), Input::Char(ch)) if ch.is_ascii_alphabetic() => {
if modifier.eq_ignore_ascii_case("ctrl") {
Ok(control_key(ch))
@@ -916,6 +987,8 @@ fn append_key(display: &mut String, input: &Input) {
Input::Escape => display.push_str("Esc"),
Input::Enter => display.push_str("Enter"),
Input::Backspace => display.push_str("Backspace"),
Input::AltBackspace => display.push_str("Alt+Backspace"),
Input::SuperBackspace => display.push_str("Cmd+Backspace"),
Input::Delete => display.push_str("Delete"),
Input::Left => display.push_str("Left"),
Input::Right => display.push_str("Right"),
@@ -1088,6 +1161,16 @@ fn default_bindings() -> HashMap<Mode, HashMap<Input, Binding>> {
.or_default()
.insert(key, Binding::Action(action));
}
#[cfg(target_os = "macos")]
for (key, action) in [
(Input::AltBackspace, BackspaceWord),
(Input::SuperBackspace, BackspaceLine),
] {
bindings
.entry(mode)
.or_default()
.insert(key, Binding::Action(action));
}
}
for mode in [
Mode::Normal,
@@ -3857,7 +3940,7 @@ impl Editor {
}
// Only the primary argument and its declared mirrors participate in these edits.
fn edit_snippet(&mut self, text: &str, deletion: Option<bool>) -> bool {
fn edit_snippet(&mut self, text: &str, deletion: Option<Action>) -> bool {
let Some(mut state) = self.snippet.take() else {
return false;
};
@@ -3873,13 +3956,9 @@ impl Editor {
let position = caret - argument.start;
let (start, end) = if state.selected {
(0, value.len())
} else if let Some(backwards) = deletion {
if backwards {
let start = value[..position]
.grapheme_indices(true)
.next_back()
.map_or(position, |(byte, _)| byte);
(start, position)
} else if let Some(action) = deletion {
if let Some(kind) = action.backspace_kind() {
(kind.start(&value[..position]), position)
} else {
let end = value[position..]
.graphemes(true)
@@ -4982,9 +5061,11 @@ impl Editor {
}
self.active.view.mode = Mode::Command;
}
Input::Backspace => {
if let Some((byte, _)) = palette.query.grapheme_indices(true).next_back() {
palette.query.truncate(byte);
input if BackspaceKind::from_input(&input).is_some() => {
if BackspaceKind::from_input(&input)
.expect("backspace input")
.truncate(&mut palette.query)
{
self.refresh_palette();
}
}
@@ -5568,6 +5649,8 @@ impl Editor {
| Action::SnippetPrevious
| Action::Newline
| Action::Backspace
| Action::BackspaceWord
| Action::BackspaceLine
| Action::Delete
)
{
@@ -5707,11 +5790,11 @@ impl Editor {
);
}
Action::Delete => {
if self.active.view.mode == Mode::Insert && self.edit_snippet("", Some(false)) {
if self.active.view.mode == Mode::Insert && self.edit_snippet("", Some(action)) {
return None;
}
let before = (self.active.view.mode != Mode::Insert).then(|| self.snapshot());
let mask = self.deletion_mask(false);
let mask = self.deletion_mask(None);
self.delete_forward(&mask);
if let Some(before) = before {
self.record_native(
@@ -5789,24 +5872,19 @@ impl Editor {
}
}
Action::Newline => self.type_native("\n"),
Action::Backspace => {
Action::Backspace | Action::BackspaceWord | Action::BackspaceLine => {
let kind = action.backspace_kind().expect("backspace action");
if self.active.view.mode == Mode::Insert {
if self.edit_snippet("", Some(true)) {
if self.edit_snippet("", Some(action)) {
return None;
}
let mask = self.deletion_mask(true);
self.backspace(&mask);
let mask = self.deletion_mask(Some(kind));
self.backspace(&mask, kind);
if mask.iter().any(|active| *active) {
self.push_insert_step(InsertStep::Backspace(mask));
self.push_insert_step(InsertStep::Backspace(mask, kind));
}
} else if let Some((byte, _)) = self
.active
.view
.command_line
.grapheme_indices(true)
.next_back()
{
self.active.view.command_line.truncate(byte);
} else {
kind.truncate(&mut self.active.view.command_line);
}
}
Action::Submit => {
@@ -7685,7 +7763,7 @@ impl Editor {
}
}
fn deletion_mask(&self, backwards: bool) -> Vec<bool> {
fn deletion_mask(&self, backwards: Option<BackspaceKind>) -> Vec<bool> {
let mut mask: Vec<_> = self
.active
.view
@@ -7693,8 +7771,8 @@ impl Editor {
.iter()
.map(|target| {
let cursor = target.active;
if backwards {
cursor.row != 0 || cursor.column != 0
if let Some(kind) = backwards {
cursor.column != 0 || (kind != BackspaceKind::Line && cursor.row != 0)
} else {
cursor.column < grapheme_count(&self.active.lines[cursor.row])
|| (self.active.view.mode == Mode::Insert
@@ -7886,16 +7964,16 @@ impl Editor {
}
InsertStep::Bulk(text) => self.insert_text(text),
InsertStep::Move(action, count) => self.move_targets(*action, *count),
InsertStep::Delete(mask) | InsertStep::Backspace(mask) => {
InsertStep::Delete(mask) | InsertStep::Backspace(mask, _) => {
// Earlier native steps may have merged carets. Refuse the entire transaction
// rather than pairing a recorded partial deletion with different targets.
if mask.len() != 1 && mask.len() != self.active.view.targets.len() {
return Err("Repeat deletion count does not match target count");
}
if matches!(step, InsertStep::Delete(_)) {
self.delete_forward(mask);
if let InsertStep::Backspace(_, kind) = step {
self.backspace(mask, *kind);
} else {
self.backspace(mask);
self.delete_forward(mask);
}
}
InsertStep::Completion {
@@ -8555,9 +8633,25 @@ impl Editor {
self.apply_edits(edits);
}
fn backspace(&mut self, mask: &[bool]) {
fn backspace_destination(&self, mut cursor: Cursor, kind: BackspaceKind) -> Cursor {
if kind == BackspaceKind::Grapheme {
return self.previous_boundary(cursor);
}
let mut prefix = &self.active.lines[cursor.row]
[..grapheme_byte(&self.active.lines[cursor.row], cursor.column)];
if kind == BackspaceKind::Word {
while cursor.row > 0 && prefix.chars().all(char::is_whitespace) {
cursor.row -= 1;
prefix = &self.active.lines[cursor.row];
}
}
cursor.column = prefix[..kind.start(prefix)].graphemes(true).count();
cursor
}
fn backspace(&mut self, mask: &[bool], kind: BackspaceKind) {
let offsets = line_offsets(&self.active.lines);
let edits = self
let mut edits: Vec<_> = self
.active
.view
.targets
@@ -8568,11 +8662,11 @@ impl Editor {
return None;
}
let cursor = item.active;
if cursor.column == 0 && cursor.row == 0 {
let destination = self.backspace_destination(cursor, kind);
if destination == cursor {
return None;
}
let start =
cursor_offset(&self.active.lines, &offsets, self.previous_boundary(cursor));
let start = cursor_offset(&self.active.lines, &offsets, destination);
let end = cursor_offset(&self.active.lines, &offsets, cursor);
Some(Edit {
target: Some(target),
@@ -8584,6 +8678,20 @@ impl Editor {
})
})
.collect();
// Backward ranges follow document-ordered carets; overlapping ranges delete once.
let mut group = 0;
for next in 1..=edits.len() {
if next < edits.len() && edits[next].start < edits[next - 1].end {
continue;
}
let start = edits[group].start;
let end = edits[next - 1].end;
for edit in &mut edits[group..next] {
edit.start = start;
edit.end = end;
}
group = next;
}
self.apply_edits(edits);
}
}
@@ -16001,6 +16109,163 @@ mod tests {
assert_eq!(editor.palette().unwrap().query, "cor");
}
#[test]
fn modified_backspace_preserves_suffixes_and_uses_unicode_boundaries() {
for (key, action, prefix, suffix, expected) in [
(
"option+backspace",
"core.backspace_word",
"alpha βeta ",
"SUFFIX\nlast",
"alpha SUFFIX\nlast",
),
(
"alt+backspace",
"core.backspace_word",
"foo don't",
"!",
"foo !",
),
(
"alt+backspace",
"core.backspace_word",
"alpha e\u{301}",
" tail",
"alpha tail",
),
(
"alt+backspace",
"core.backspace_word",
"alpha 👩‍🚀",
" tail",
"alpha tail",
),
(
"alt+backspace",
"core.backspace_word",
"one word\n ",
"tail",
"one tail",
),
(
"cmd+backspace",
"core.backspace_line",
"first\n βeta ",
"SUFFIX\nlast",
"first\nSUFFIX\nlast",
),
(
"super+backspace",
"core.backspace_line",
"first\n",
"tail",
"first\ntail",
),
("alt+backspace", "core.backspace_word", "", "tail", "tail"),
] {
let mut editor = editor();
editor.bind("insert", key, Some(action.into())).unwrap();
let original = format!("{prefix}{suffix}");
editor
.apply(vec![Effect::Replace(original.clone())])
.unwrap();
let row = prefix.bytes().filter(|&byte| byte == b'\n').count();
let column = prefix.rsplit('\n').next().unwrap().graphemes(true).count();
set_carets(&mut editor, &[(row, column)], 0);
let undo = editor.active.undo.len();
let revision = editor.text_revision();
editor.handle(Input::Char('i'));
editor.handle(parse_key(key).unwrap());
assert_eq!(editor.context().text, expected, "{prefix:?}");
editor.handle(Input::Escape);
if original == expected {
assert_eq!(editor.text_revision(), revision);
assert_eq!(editor.active.undo.len(), undo);
} else {
assert_eq!(editor.active.undo.len(), undo + 1);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, original);
assert_eq!(editor.cursor(), Cursor { row, column });
}
}
}
#[test]
fn modified_backspace_merges_overlapping_carets_and_repeats_new_boundaries() {
for (key, action, expected) in [
(
"alt+backspace",
"core.backspace_word",
"foo tail\nkeep row",
),
("cmd+backspace", "core.backspace_line", " tail\n row"),
] {
let mut editor = editor();
editor.bind("insert", key, Some(action.into())).unwrap();
let original = "foo elephant tail\nkeep second row";
editor
.apply(vec![Effect::Replace(original.into())])
.unwrap();
set_carets(&mut editor, &[(0, 5), (0, 12), (1, 11)], 1);
let carets = editor.carets();
editor.handle(Input::Char('i'));
editor.handle(parse_key(key).unwrap());
editor.handle(Input::Escape);
assert_eq!(editor.context().text, expected);
assert_eq!(editor.selection_count(), 2);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, original);
assert_eq!(editor.carets(), carets);
let next = "foo hippopotamus tail\nkeep longer row";
editor.apply(vec![Effect::Replace(next.into())]).unwrap();
set_carets(&mut editor, &[(0, 5), (0, 16), (1, 11)], 1);
editor.handle(Input::Char('.'));
assert_eq!(editor.context().text, expected);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, next);
}
}
#[test]
fn modified_backspace_queries_never_edit_the_underlying_buffer() {
for opening in [Input::Char(':'), Input::Char('/'), Input::Ctrl('p')] {
let mut editor = editor();
for mode in ["command", "search"] {
editor
.bind(mode, "alt+backspace", Some("core.backspace_word".into()))
.unwrap();
editor
.bind(mode, "cmd+backspace", Some("core.backspace_line".into()))
.unwrap();
}
editor
.apply(vec![Effect::Replace("buffer text".into())])
.unwrap();
let revision = editor.text_revision();
let carets = editor.carets();
editor.handle(opening);
type_text(&mut editor, "alpha βeta ");
editor.handle(Input::AltBackspace);
assert_eq!(
editor
.palette()
.map_or(editor.command_line(), |palette| palette.query.as_str()),
"alpha "
);
editor.handle(Input::SuperBackspace);
assert_eq!(
editor
.palette()
.map_or(editor.command_line(), |palette| palette.query.as_str()),
""
);
assert_eq!(editor.context().text, "buffer text");
assert_eq!(editor.text_revision(), revision);
assert_eq!(editor.carets(), carets);
}
}
#[test]
fn tree_leaf_search_opens_direct_match_and_directory_search_enters_browse() {
let directory = Directory::new();
@@ -21291,6 +21556,47 @@ mod tests {
assert!(editor.snippet_selections().is_empty());
}
#[test]
fn modified_backspace_snippets_stay_inside_arguments_and_update_mirrors() {
let mut editor = editor();
editor
.bind(
"insert",
"alt+backspace",
Some("core.backspace_word".into()),
)
.unwrap();
editor
.bind(
"insert",
"cmd+backspace",
Some("core.backspace_line".into()),
)
.unwrap();
editor.handle(Input::Char('i'));
editor
.show_completion(vec![snippet_item(
(0, 0),
(0, 0),
"f(one, one)",
vec![(1, vec![2..5, 7..10]), (0, vec![11..11])],
)])
.unwrap();
type_text(&mut editor, "alpha βeta");
editor.handle(Input::AltBackspace);
assert_eq!(editor.context().text, "f(alpha , alpha )");
editor.handle(Input::SuperBackspace);
assert_eq!(editor.context().text, "f(, )");
editor.handle(Input::SuperBackspace);
editor.handle(Input::AltBackspace);
assert_eq!(editor.context().text, "f(, )");
type_text(&mut editor, "keep");
assert_eq!(editor.context().text, "f(keep, keep)");
editor.handle(Input::Escape);
editor.handle(Input::Char('u'));
assert_eq!(editor.context().text, "");
}
#[test]
fn snippet_nested_mirrors_update_parents_and_replacing_parent_retires_children() {
let mut editor = editor();
+52 -14
View File
@@ -3,7 +3,6 @@ use std::collections::{HashMap, HashSet};
use std::fs;
use std::io;
use std::path::{Path, PathBuf};
use unicode_segmentation::UnicodeSegmentation;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum FilePresentation {
@@ -385,10 +384,20 @@ impl FilePicker {
}
pub(crate) fn handle_filter_input(&mut self, input: &crate::editor::Input) -> bool {
use crate::editor::Input;
use crate::editor::{BackspaceKind, Input};
match input {
Input::Ctrl('u') => self.clear_filter(),
Input::Backspace if self.view == FileView::List || self.searching => self.backspace(),
input
if BackspaceKind::from_input(input).is_some()
&& (self.view == FileView::List || self.searching) =>
{
if BackspaceKind::from_input(input)
.expect("guard checked backspace input")
.truncate(&mut self.query)
{
self.refilter();
}
}
Input::Char('i') if self.view == FileView::Tree && !self.searching => {
self.start_search()
}
@@ -604,13 +613,6 @@ impl FilePicker {
self.clear_error();
}
pub(crate) fn backspace(&mut self) {
if let Some((byte, _)) = self.query.grapheme_indices(true).next_back() {
self.query.truncate(byte);
self.refilter();
}
}
pub(crate) fn navigate(&mut self, forward: bool) {
let count = self.rows.len();
self.selected = if count == 0 {
@@ -932,11 +934,11 @@ mod tests {
picker.refilter();
assert_eq!(paths(&picker), [PathBuf::from("a space 界[e\u{301}].txt")]);
picker.query = "e\u{301}👩‍💻".into();
picker.backspace();
assert!(picker.handle_filter_input(&crate::editor::Input::Backspace));
assert_eq!(picker.query, "e\u{301}");
picker.backspace();
assert!(picker.handle_filter_input(&crate::editor::Input::Backspace));
assert_eq!(picker.query, "");
picker.backspace();
assert!(picker.handle_filter_input(&crate::editor::Input::Backspace));
assert_eq!(picker.entries().len(), 3);
picker.query = "no match".into();
picker.refilter();
@@ -947,12 +949,48 @@ mod tests {
}
}
#[test]
fn word_and_line_deletion_refilter_only_editable_queries() {
use crate::editor::Input;
for view in [FileView::List, FileView::Tree] {
let mut picker = FilePicker::from_paths(
PathBuf::new(),
["alpha one.txt", "alpha two.txt", "beta.txt"]
.map(PathBuf::from)
.into_iter()
.enumerate(),
);
picker.set_view(view);
picker.query = "alpha missing".into();
picker.refilter();
assert_eq!(picker.entries().len(), 0);
if view == FileView::Tree {
for input in [Input::Backspace, Input::AltBackspace, Input::SuperBackspace] {
assert!(!picker.handle_filter_input(&input));
assert_eq!(picker.query, "alpha missing");
}
picker.start_search();
}
assert!(picker.handle_filter_input(&Input::AltBackspace));
assert_eq!(picker.query, "alpha ");
assert_eq!(picker.entries().len(), 2);
assert!(picker.handle_filter_input(&Input::SuperBackspace));
assert_eq!(picker.query, "");
assert_eq!(picker.entries().len(), 3);
for input in [Input::Backspace, Input::AltBackspace, Input::SuperBackspace] {
assert!(picker.handle_filter_input(&input));
assert_eq!(picker.entries().len(), 3);
}
}
}
#[test]
fn empty_scan_refresh_and_failure_preserve_reachable_state() {
let directory = Directory::new();
let mut picker = directory.scan();
picker.navigate(false);
picker.backspace();
assert!(picker.handle_filter_input(&crate::editor::Input::Backspace));
assert_eq!(picker.entries().len(), 0);
directory.put("new.txt", "text");
picker.query = "new".into();
+61
View File
@@ -882,6 +882,14 @@ fn typing_ready(
}
fn translate(key: KeyEvent) -> Option<Input> {
#[cfg(target_os = "macos")]
if key.code == KeyCode::Backspace {
match key.modifiers {
KeyModifiers::ALT => return Some(Input::AltBackspace),
KeyModifiers::SUPER => return Some(Input::SuperBackspace),
_ => {}
}
}
if key
.modifiers
.intersects(KeyModifiers::SUPER | KeyModifiers::HYPER | KeyModifiers::META)
@@ -10025,6 +10033,59 @@ end
}
}
#[cfg(target_os = "macos")]
#[test]
fn macos_modified_backspace_edits_words_and_line_prefix_from_terminal_events() {
let directory = Directory::new();
let path = directory.file("input.txt", "before\nalpha βeta SUFFIX\nlast");
let mut session = EditorSession::open(Some(path), vec![], None).unwrap();
session.handle(Input::Down);
session.handle(Input::Home);
session.handle(Input::Char('i'));
for _ in 0..13 {
session.handle(Input::Right);
}
session.handle(translate(KeyEvent::new(KeyCode::Backspace, KeyModifiers::ALT)).unwrap());
assert_eq!(session.editor().lines(), ["before", "alpha SUFFIX", "last"]);
session.handle(translate(KeyEvent::new(KeyCode::Backspace, KeyModifiers::SUPER)).unwrap());
assert_eq!(session.editor().lines(), ["before", "SUFFIX", "last"]);
for modifiers in [
KeyModifiers::META,
KeyModifiers::HYPER,
KeyModifiers::ALT | KeyModifiers::CONTROL,
KeyModifiers::ALT | KeyModifiers::SHIFT,
KeyModifiers::SUPER | KeyModifiers::CONTROL,
KeyModifiers::SUPER | KeyModifiers::SHIFT,
KeyModifiers::SUPER | KeyModifiers::ALT,
KeyModifiers::SUPER | KeyModifiers::META,
] {
assert_eq!(
translate(KeyEvent::new(KeyCode::Backspace, modifiers)),
None
);
}
assert_eq!(
translate(KeyEvent::new(KeyCode::Char('a'), KeyModifiers::SUPER)),
None
);
}
#[cfg(not(target_os = "macos"))]
#[test]
fn modified_backspace_keeps_non_macos_terminal_defaults() {
for modifiers in [KeyModifiers::ALT, KeyModifiers::SUPER] {
assert_eq!(
translate(KeyEvent::new(KeyCode::Backspace, modifiers)),
None
);
}
assert_eq!(
translate(KeyEvent::new(KeyCode::Backspace, KeyModifiers::CONTROL)),
Some(Input::Backspace)
);
}
#[test]
fn wheel_routes_to_floating_and_unfocused_docked_files_without_changing_selection() {
let directory = Directory::new();