vim: Add support for toggling boolean values (#25997)
Closes #10400 Closes https://github.com/zed-industries/zed/issues/17947 Changes: - Let vim::increment find boolean values in the line and toggle them. Release Notes: - vim: Added support for toggling boolean values with `ctrl-a`/`ctrl-x`
This commit is contained in:
parent
82338e2c47
commit
3e64f38ba0
1 changed files with 162 additions and 1 deletions
|
@ -7,6 +7,8 @@ use std::ops::Range;
|
||||||
|
|
||||||
use crate::{state::Mode, Vim};
|
use crate::{state::Mode, Vim};
|
||||||
|
|
||||||
|
const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")];
|
||||||
|
|
||||||
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
|
||||||
#[serde(deny_unknown_fields)]
|
#[serde(deny_unknown_fields)]
|
||||||
struct Increment {
|
struct Increment {
|
||||||
|
@ -77,6 +79,13 @@ impl Vim {
|
||||||
if selection.is_empty() {
|
if selection.is_empty() {
|
||||||
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||||
}
|
}
|
||||||
|
} else if let Some((range, boolean)) = find_boolean(&snapshot, start) {
|
||||||
|
let replace = toggle_boolean(&boolean);
|
||||||
|
delta += step as i64;
|
||||||
|
edits.push((range.clone(), replace));
|
||||||
|
if selection.is_empty() {
|
||||||
|
new_anchors.push((false, snapshot.anchor_after(range.end)))
|
||||||
|
}
|
||||||
} else if selection.is_empty() {
|
} else if selection.is_empty() {
|
||||||
new_anchors.push((true, snapshot.anchor_after(start)))
|
new_anchors.push((true, snapshot.anchor_after(start)))
|
||||||
}
|
}
|
||||||
|
@ -243,11 +252,104 @@ fn find_number(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn find_boolean(snapshot: &MultiBufferSnapshot, start: Point) -> Option<(Range<Point>, String)> {
|
||||||
|
let mut offset = start.to_offset(snapshot);
|
||||||
|
|
||||||
|
let ch0 = snapshot.chars_at(offset).next();
|
||||||
|
if ch0.as_ref().is_some_and(|c| c.is_ascii_alphabetic()) {
|
||||||
|
for ch in snapshot.reversed_chars_at(offset) {
|
||||||
|
if ch.is_ascii_alphabetic() {
|
||||||
|
offset -= ch.len_utf8();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut begin = None;
|
||||||
|
let mut end = None;
|
||||||
|
let mut word = String::new();
|
||||||
|
|
||||||
|
let mut chars = snapshot.chars_at(offset);
|
||||||
|
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch.is_ascii_alphabetic() {
|
||||||
|
if begin.is_none() {
|
||||||
|
begin = Some(offset);
|
||||||
|
}
|
||||||
|
word.push(ch);
|
||||||
|
} else if begin.is_some() {
|
||||||
|
end = Some(offset);
|
||||||
|
let word_lower = word.to_lowercase();
|
||||||
|
if BOOLEAN_PAIRS
|
||||||
|
.iter()
|
||||||
|
.any(|(a, b)| word_lower == *a || word_lower == *b)
|
||||||
|
{
|
||||||
|
return Some((
|
||||||
|
begin.unwrap().to_point(snapshot)..end.unwrap().to_point(snapshot),
|
||||||
|
word,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
begin = None;
|
||||||
|
end = None;
|
||||||
|
word = String::new();
|
||||||
|
} else if ch == '\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
offset += ch.len_utf8();
|
||||||
|
}
|
||||||
|
if let Some(begin) = begin {
|
||||||
|
let end = end.unwrap_or(offset);
|
||||||
|
let word_lower = word.to_lowercase();
|
||||||
|
if BOOLEAN_PAIRS
|
||||||
|
.iter()
|
||||||
|
.any(|(a, b)| word_lower == *a || word_lower == *b)
|
||||||
|
{
|
||||||
|
return Some((begin.to_point(snapshot)..end.to_point(snapshot), word));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn toggle_boolean(boolean: &str) -> String {
|
||||||
|
let lower = boolean.to_lowercase();
|
||||||
|
|
||||||
|
let target = BOOLEAN_PAIRS
|
||||||
|
.iter()
|
||||||
|
.find_map(|(a, b)| {
|
||||||
|
if lower == *a {
|
||||||
|
Some(b)
|
||||||
|
} else if lower == *b {
|
||||||
|
Some(a)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or(&boolean);
|
||||||
|
|
||||||
|
if boolean.chars().all(|c| c.is_uppercase()) {
|
||||||
|
// Upper case
|
||||||
|
target.to_uppercase()
|
||||||
|
} else if boolean.chars().next().unwrap_or(' ').is_uppercase() {
|
||||||
|
// Title case
|
||||||
|
let mut chars = target.chars();
|
||||||
|
match chars.next() {
|
||||||
|
None => String::new(),
|
||||||
|
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
target.to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
use indoc::indoc;
|
use indoc::indoc;
|
||||||
|
|
||||||
use crate::test::NeovimBackedTestContext;
|
use crate::{
|
||||||
|
state::Mode,
|
||||||
|
test::{NeovimBackedTestContext, VimTestContext},
|
||||||
|
};
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_increment(cx: &mut gpui::TestAppContext) {
|
async fn test_increment(cx: &mut gpui::TestAppContext) {
|
||||||
|
@ -599,4 +701,63 @@ mod test {
|
||||||
24
|
24
|
||||||
30"});
|
30"});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_toggle_boolean(cx: &mut gpui::TestAppContext) {
|
||||||
|
let mut cx = VimTestContext::new(cx, true).await;
|
||||||
|
|
||||||
|
cx.set_state("let enabled = trˇue;", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("ctrl-a");
|
||||||
|
cx.assert_state("let enabled = falsˇe;", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes("0 ctrl-a");
|
||||||
|
cx.assert_state("let enabled = truˇe;", Mode::Normal);
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
ˇlet enabled = TRUE;
|
||||||
|
let enabled = TRUE;
|
||||||
|
let enabled = TRUE;
|
||||||
|
"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
cx.simulate_keystrokes("shift-v j j ctrl-x");
|
||||||
|
cx.assert_state(
|
||||||
|
indoc! {"
|
||||||
|
ˇlet enabled = FALSE;
|
||||||
|
let enabled = FALSE;
|
||||||
|
let enabled = FALSE;
|
||||||
|
"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.set_state(
|
||||||
|
indoc! {"
|
||||||
|
let enabled = ˇYes;
|
||||||
|
let enabled = Yes;
|
||||||
|
let enabled = Yes;
|
||||||
|
"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
cx.simulate_keystrokes("ctrl-v j j e ctrl-x");
|
||||||
|
cx.assert_state(
|
||||||
|
indoc! {"
|
||||||
|
let enabled = ˇNo;
|
||||||
|
let enabled = No;
|
||||||
|
let enabled = No;
|
||||||
|
"},
|
||||||
|
Mode::Normal,
|
||||||
|
);
|
||||||
|
|
||||||
|
cx.set_state("ˇlet enabled = True;", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("ctrl-a");
|
||||||
|
cx.assert_state("let enabled = Falsˇe;", Mode::Normal);
|
||||||
|
|
||||||
|
cx.simulate_keystrokes("ctrl-a");
|
||||||
|
cx.assert_state("let enabled = Truˇe;", Mode::Normal);
|
||||||
|
|
||||||
|
cx.set_state("let enabled = Onˇ;", Mode::Normal);
|
||||||
|
cx.simulate_keystrokes("v b ctrl-a");
|
||||||
|
cx.assert_state("let enabled = ˇOff;", Mode::Normal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue