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:
brian tan 2025-03-04 22:00:44 -05:00 committed by GitHub
parent 82338e2c47
commit 3e64f38ba0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -7,6 +7,8 @@ use std::ops::Range;
use crate::{state::Mode, Vim};
const BOOLEAN_PAIRS: &[(&str, &str)] = &[("true", "false"), ("yes", "no"), ("on", "off")];
#[derive(Clone, Deserialize, JsonSchema, PartialEq)]
#[serde(deny_unknown_fields)]
struct Increment {
@ -77,6 +79,13 @@ impl Vim {
if selection.is_empty() {
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() {
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)]
mod test {
use indoc::indoc;
use crate::test::NeovimBackedTestContext;
use crate::{
state::Mode,
test::{NeovimBackedTestContext, VimTestContext},
};
#[gpui::test]
async fn test_increment(cx: &mut gpui::TestAppContext) {
@ -599,4 +701,63 @@ mod test {
24
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);
}
}