Skip to content

Commit 1eaaa6c

Browse files
committed
feat(vi): visual mode u, U
1 parent 30d61db commit 1eaaa6c

4 files changed

Lines changed: 100 additions & 3 deletions

File tree

src/core_editor/editor.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,9 @@ impl Editor {
140140
EditCommand::SelectAll => self.select_all(),
141141
EditCommand::CutSelection => self.cut_selection_to_cut_buffer(),
142142
EditCommand::CopySelection => self.copy_selection_to_cut_buffer(),
143+
EditCommand::LowercaseSelection => self.lowercase_selection(),
144+
EditCommand::UppercaseSelection => self.uppercase_selection(),
145+
EditCommand::SwitchcaseSelection => self.switchcase_selection(),
143146
EditCommand::Paste => self.paste_cut_buffer(),
144147
EditCommand::CopyFromStart => self.copy_from_start(),
145148
EditCommand::CopyFromStartLinewise => self.copy_from_start_linewise(),
@@ -674,6 +677,37 @@ impl Editor {
674677
}
675678
}
676679

680+
fn lowercase_selection(&mut self) {
681+
if let Some((start, end)) = self.get_selection() {
682+
let lowercase_slice = self.line_buffer.get_buffer()[start..end].to_ascii_lowercase();
683+
self.line_buffer.replace_range(start..end, &lowercase_slice);
684+
}
685+
}
686+
687+
fn uppercase_selection(&mut self) {
688+
if let Some((start, end)) = self.get_selection() {
689+
let uppercase_slice = self.line_buffer.get_buffer()[start..end].to_ascii_uppercase();
690+
self.line_buffer.replace_range(start..end, &uppercase_slice);
691+
}
692+
}
693+
694+
fn switchcase_selection(&mut self) {
695+
if let Some((start, end)) = self.get_selection() {
696+
let switchcase_slice = self.line_buffer.get_buffer()[start..end]
697+
.chars()
698+
.map(|ch| {
699+
if ch.is_ascii_lowercase() {
700+
ch.to_ascii_uppercase()
701+
} else {
702+
ch.to_ascii_lowercase()
703+
}
704+
})
705+
.collect::<String>();
706+
self.line_buffer
707+
.replace_range(start..end, &switchcase_slice);
708+
}
709+
}
710+
677711
/// If a selection is active returns the selected range, otherwise None.
678712
/// The range is guaranteed to be ascending.
679713
pub fn get_selection(&self) -> Option<(usize, usize)> {
@@ -1926,6 +1960,30 @@ mod test {
19261960
assert_eq!(editor.cut_buffer.get().0, expected_cut);
19271961
}
19281962

1963+
#[rstest]
1964+
#[case("foo bar baz", 0, 3, EditCommand::LowercaseSelection, "foo bar baz")]
1965+
#[case("Foo Bar Baz", 0, 7, EditCommand::LowercaseSelection, "foo bar Baz")]
1966+
#[case("FOO BAR BAZ", 0, 12, EditCommand::LowercaseSelection, "foo bar baz")]
1967+
#[case("foo bar baz", 0, 3, EditCommand::UppercaseSelection, "FOO bar baz")]
1968+
#[case("Foo Bar Baz", 0, 7, EditCommand::UppercaseSelection, "FOO BAR Baz")]
1969+
#[case("FOO BAR BAZ", 0, 12, EditCommand::UppercaseSelection, "FOO BAR BAZ")]
1970+
#[case("foo bar baz", 0, 3, EditCommand::SwitchcaseSelection, "FOO bar baz")]
1971+
#[case("Foo Bar Baz", 0, 7, EditCommand::SwitchcaseSelection, "fOO bAR Baz")]
1972+
#[case("FOO BAR BAZ", 0, 12, EditCommand::SwitchcaseSelection, "foo bar baz")]
1973+
fn test_lower_upper_switchcase_selection(
1974+
#[case] input: &str,
1975+
#[case] selection_start: usize,
1976+
#[case] selection_end: usize,
1977+
#[case] command: EditCommand,
1978+
#[case] expected_buffer: &str,
1979+
) {
1980+
let mut editor = editor_with(input);
1981+
editor.move_to_position(selection_start, false);
1982+
editor.move_to_position(selection_end, true);
1983+
editor.run_edit_command(&command);
1984+
assert_eq!(editor.get_buffer(), expected_buffer);
1985+
}
1986+
19291987
#[rstest]
19301988
#[case("hello-world test", 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::Word }, "-world test", "hello")] // small word gets just "hello"
19311989
#[case("hello-world test", 2, TextObject { scope: TextObjectScope::Inner, object_type: TextObjectType::BigWord }, " test", "hello-world")] // big word gets "hello-word"

src/edit_mode/vi/command.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ where
7878
let _ = input.next();
7979
Some(Command::EnterViAppend)
8080
}
81-
Some('u') => {
81+
Some('u') if mode == ViMode::Normal => {
8282
let _ = input.next();
8383
Some(Command::Undo)
8484
}
@@ -168,6 +168,14 @@ where
168168
// This arm should be unreachable
169169
ViMode::Insert => None,
170170
},
171+
Some(&&u @ ('u' | 'U')) if mode == ViMode::Visual => {
172+
let _ = input.next();
173+
if u.is_ascii_lowercase() {
174+
Some(Command::Lowercase)
175+
} else {
176+
Some(Command::Uppercase)
177+
}
178+
}
171179
_ => None,
172180
}
173181
}
@@ -193,6 +201,8 @@ pub enum Command {
193201
RewriteCurrentLine,
194202
Change,
195203
HistorySearch,
204+
Lowercase,
205+
Uppercase,
196206
Switchcase,
197207
RepeatLastAction,
198208
Yank,
@@ -260,7 +270,19 @@ impl Command {
260270
}
261271
}
262272
Self::HistorySearch => vec![ReedlineOption::Event(ReedlineEvent::SearchHistory)],
263-
Self::Switchcase => vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)],
273+
Self::Lowercase => {
274+
vec![ReedlineOption::Edit(EditCommand::LowercaseSelection)]
275+
}
276+
Self::Uppercase => {
277+
vec![ReedlineOption::Edit(EditCommand::UppercaseSelection)]
278+
}
279+
Self::Switchcase => {
280+
if vi_state.mode == ViMode::Visual {
281+
vec![ReedlineOption::Edit(EditCommand::SwitchcaseSelection)]
282+
} else {
283+
vec![ReedlineOption::Edit(EditCommand::SwitchcaseChar)]
284+
}
285+
}
264286
// Whenever a motion is required to finish the command we must be in visual mode
265287
Self::Delete | Self::Change => vec![ReedlineOption::Edit(EditCommand::CutSelection)],
266288
Self::Yank => vec![ReedlineOption::Edit(EditCommand::CopySelection)],

src/edit_mode/vi/parser.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,12 @@ impl ParsedViSequence {
112112
(Some(Command::Change), ParseResult::Incomplete) if mode == ViMode::Visual => {
113113
Some(ViMode::Insert)
114114
}
115-
(Some(Command::Delete), ParseResult::Incomplete) if mode == ViMode::Visual => {
115+
(Some(Command::Delete), ParseResult::Incomplete)
116+
| (Some(Command::Lowercase), ParseResult::Incomplete)
117+
| (Some(Command::Uppercase), ParseResult::Incomplete)
118+
| (Some(Command::Switchcase), ParseResult::Incomplete)
119+
if mode == ViMode::Visual =>
120+
{
116121
Some(ViMode::Normal)
117122
}
118123
(Some(Command::ChangeInsidePair { .. }), _) => Some(ViMode::Insert),

src/enums.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,6 +390,15 @@ pub enum EditCommand {
390390
/// Copy selection to local buffer
391391
CopySelection,
392392

393+
/// LowercaseSelection
394+
LowercaseSelection,
395+
396+
/// Uppercase selection
397+
UppercaseSelection,
398+
399+
/// Switchcase selection
400+
SwitchcaseSelection,
401+
393402
/// Paste content from local buffer at the current cursor position
394403
Paste,
395404

@@ -583,6 +592,9 @@ impl EditCommand {
583592
| EditCommand::CutLeftUntil(_)
584593
| EditCommand::CutLeftBefore(_)
585594
| EditCommand::CutSelection
595+
| EditCommand::LowercaseSelection
596+
| EditCommand::UppercaseSelection
597+
| EditCommand::SwitchcaseSelection
586598
| EditCommand::Paste
587599
| EditCommand::CutInsidePair { .. }
588600
| EditCommand::CutAroundPair { .. }

0 commit comments

Comments
 (0)