//! This module is for "block objects" i.e. text objects delimited by brackets/parentheses. use std::ops::Range; use itertools::Itertools; use string_offset::CharOffset; use warpui_core::text::TextBuffer; use crate::vim::{BracketChar, BracketEnd, BracketType}; use crate::vim_find_matching_bracket; /// Vim's block-based text objects, e.g. `di{`. This includes a string of text enclosed by any pair /// of [`BracketType`], not including the brackets themselves. /// See https://vimdoc.sourceforge.net/htmldoc/motion.html#ib /// or enter ":help i{" in Vim. /// /// There may be whitespace in within the brackets which is also not included. If the closing /// bracket is on a different line then the opening bracket, and there is only whitespace between /// the closing bracket and the newline preceding it, all that space plus the newline are _not_ /// included in the text object. (AFAIK this is not documented.) We can refer to that space as /// "trailing padding". Furthermore, the `c` and `d` commands differ in how they respect "leading /// padding", i.e. the whitespace plus newline after the opening bracket, in that `d` will delete /// the leading padding but `c` will not. The `preserve_leading_padding` parameter controls this. /// /// Returns None when the buffer is empty, the offset is out of bounds, or there is no matching /// pair of brackets of the type we're looking for around the cursor. pub fn vim_inner_block( buffer: &T, offset: C, bracket_type: BracketType, preserve_leading_padding: bool, ) -> Option> where T: TextBuffer + ?Sized, C: Into, { let block_range = vim_a_block(buffer, offset, bracket_type)?; let (mut block_start, mut block_end) = (block_range.start, block_range.end); // Move bounds by 1 to remove the brackets, though we may need to trim any trailing or leading // padding. block_start += 1; block_end -= 1; // First, check if we need to remove trailing padding from the range. if let Some((i, _)) = buffer .chars_rev_at(block_end) .ok()? .take_while(|c| c.is_whitespace()) .find_position(|c| *c == '\n') { block_end -= i + 1; } // Finally, if we're doing a command that preserves leading padding, e.g. `c`, check if we need // to remove that from the range. if preserve_leading_padding && let Some((i, _)) = buffer .chars_at(block_start) .ok()? .take_while(|c| c.is_whitespace()) .find_position(|c| *c == '\n') { block_start += i + 1; } Some(block_start..block_end) } /// Vim's block-based text objects, e.g. `da{`. This includes a string of text enclosed by any pair /// of [`BracketType`], including the brackets themselves. /// See https://vimdoc.sourceforge.net/htmldoc/motion.html#ab /// or enter ":help a{" in Vim. /// /// Returns None when the buffer is empty, the offset is out of bounds, or there is no matching /// pair of brackets of the type we're looking for around the cursor. pub fn vim_a_block( buffer: &T, offset: C, bracket_type: BracketType, ) -> Option> where T: TextBuffer + ?Sized, C: Into, { let offset = offset.into(); // If the buffer is empty, return early. let c = buffer.chars_at(offset).ok()?.next()?; // Check if the cursor is already on a bracket of the type we're looking for. match BracketChar::try_from(c) { // If it is, just call [`vim_find_matching_bracket`] for the other bracket. Ok(bracket) if bracket.kind == bracket_type => { let other_offset = vim_find_matching_bracket(buffer, &bracket, offset)?; // We may have just moved backwards or forwards. Make sure the smaller offset comes // first in the range bounds. let (start_offset, end_offset) = if other_offset > offset { (offset, other_offset) } else { (other_offset, offset) }; Some(start_offset..end_offset + 1) } // If not, perform the search in both directions. _ => { let end_offset = vim_find_matching_bracket( buffer, &BracketChar { end: BracketEnd::Opening, kind: bracket_type, }, offset, )?; let start_offset = vim_find_matching_bracket( buffer, &BracketChar { end: BracketEnd::Closing, kind: bracket_type, }, end_offset, )?; Some(start_offset..end_offset + 1) } } } #[cfg(test)] #[path = "block_tests.rs"] mod tests;