From 3e64f38ba0c1a8e0243a3ebf3a49f5cb0d943f63 Mon Sep 17 00:00:00 2001 From: brian tan Date: Tue, 4 Mar 2025 22:00:44 -0500 Subject: [PATCH] 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` --- crates/vim/src/normal/increment.rs | 163 ++++++++++++++++++++++++++++- 1 file changed, 162 insertions(+), 1 deletion(-) diff --git a/crates/vim/src/normal/increment.rs b/crates/vim/src/normal/increment.rs index df31b1fcc887bfa170f0f4787d61b2dbd4d06991..f404004f95587a9fe2620f31c8ba5a00149b7714 100644 --- a/crates/vim/src/normal/increment.rs +++ b/crates/vim/src/normal/increment.rs @@ -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, 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::() + 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); + } }