use std::{
fmt::{Display, Formatter},
ops::Range,
};
use crate::text_stream::{TextByteStream, TextRange, TextStreamError, ValidatedTextRange};
use super::{
Counted, Motion, OperatorTargetSource, VimSelection, VisualMode,
motion::{self, ColumnMotion, LineAddress},
};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum TargetKind {
Characterwise,
Linewise,
Blockwise,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct OperatorTarget {
kind: TargetKind,
range: ValidatedOperatorRange,
}
impl OperatorTarget {
pub fn characterwise(
stream: &TextByteStream,
range: Range<usize>,
) -> Result<Self, TargetResolutionError> {
Self::new(stream, TargetKind::Characterwise, range)
}
pub fn linewise(
stream: &TextByteStream,
range: Range<usize>,
) -> Result<Self, TargetResolutionError> {
Self::new(stream, TargetKind::Linewise, range)
}
pub fn from_visual_selection(
stream: &TextByteStream,
selection: VimSelection,
cursor_byte_index: usize,
visual_mode: VisualMode,
) -> Result<Self, TargetResolutionError> {
let text = stream.as_str();
match visual_mode {
VisualMode::Characterwise => Self::characterwise(
stream,
selection.characterwise_byte_range(text, cursor_byte_index),
),
VisualMode::Linewise => Self::linewise(
stream,
selection.linewise_operator_byte_range(text, cursor_byte_index),
),
VisualMode::Blockwise => Err(TargetResolutionError::UnsupportedTarget),
}
}
pub fn from_source(
stream: &TextByteStream,
cursor_byte_index: usize,
source: OperatorTargetSource,
) -> Result<Self, TargetResolutionError> {
let text = stream.as_str();
match source {
OperatorTargetSource::Motion(counted) => Self::from_counted_motion(
stream,
motion::clamp_to_cursor_position(text, cursor_byte_index),
counted,
),
OperatorTargetSource::CurrentLine { count } => Self::current_line_count(
stream,
motion::clamp_to_boundary(text, cursor_byte_index),
count.get(),
),
OperatorTargetSource::TextObject(object) => {
let range = super::resolve_text_object_range(
text,
cursor_byte_index,
object.item,
object.count.get(),
)?;
Self::characterwise(stream, range)
}
OperatorTargetSource::VisualSelection { selection, mode } => {
Self::from_visual_selection(stream, selection, cursor_byte_index, mode)
}
}
}
pub fn from_normal_source(
stream: &TextByteStream,
cursor_byte_index: usize,
source: OperatorTargetSource,
) -> Result<Self, TargetResolutionError> {
match source {
OperatorTargetSource::VisualSelection { .. } => Err(TargetResolutionError::WrongMode),
source => Self::from_source(stream, cursor_byte_index, source),
}
}
pub fn current_line(
stream: &TextByteStream,
cursor_byte_index: usize,
) -> Result<Self, TargetResolutionError> {
Self::current_line_count(stream, cursor_byte_index, 1)
}
fn current_line_count(
stream: &TextByteStream,
cursor_byte_index: usize,
count: usize,
) -> Result<Self, TargetResolutionError> {
let text = stream.as_str();
let cursor = motion::clamp_to_boundary(text, cursor_byte_index);
let start = line_start(text, cursor);
let end = (1..count).fold(line_end_including_newline(text, cursor), |end, _line| {
if end >= text.len() {
end
} else {
line_end_including_newline(text, end)
}
});
Self::linewise(stream, start..end)
}
#[must_use]
pub const fn kind(self) -> TargetKind {
self.kind
}
#[must_use]
pub const fn range(self) -> ValidatedOperatorRange {
self.range
}
fn new(
stream: &TextByteStream,
kind: TargetKind,
range: Range<usize>,
) -> Result<Self, TargetResolutionError> {
Ok(Self {
kind,
range: ValidatedOperatorRange::new(stream, range)?,
})
}
fn from_counted_motion(
stream: &TextByteStream,
cursor_byte_index: usize,
counted: Counted<Motion>,
) -> Result<Self, TargetResolutionError> {
let text = stream.as_str();
if is_linewise_motion(counted.item) {
return Self::linewise_motion(stream, cursor_byte_index, counted);
}
let destination = apply_counted_motion_for_target(text, cursor_byte_index, counted);
let range = characterwise_motion_range(text, cursor_byte_index, destination, counted.item);
Self::characterwise(stream, range)
}
fn linewise_motion(
stream: &TextByteStream,
cursor_byte_index: usize,
counted: Counted<Motion>,
) -> Result<Self, TargetResolutionError> {
let text = stream.as_str();
let destination = apply_counted_motion_for_target(text, cursor_byte_index, counted);
let start = line_start(text, cursor_byte_index.min(destination));
let end = line_end_including_newline(text, cursor_byte_index.max(destination));
Self::linewise(stream, start..end)
}
}
fn apply_counted_motion_for_target(
text: &str,
cursor_byte_index: usize,
counted: Counted<Motion>,
) -> usize {
match counted.item {
Motion::LineAddress(_) => motion::apply_motion(text, cursor_byte_index, counted.item),
Motion::Column(ColumnMotion::ScreenColumn) => {
motion::apply_screen_column_motion(text, cursor_byte_index, counted.count.get())
}
motion_item => (0..counted.count.get()).fold(cursor_byte_index, |index, _step| {
motion::apply_motion(text, index, motion_item)
}),
}
}
fn characterwise_motion_range(
text: &str,
cursor_byte_index: usize,
destination: usize,
motion: Motion,
) -> Range<usize> {
if destination < cursor_byte_index {
return destination..cursor_byte_index;
}
let end = if matches!(motion, Motion::Column(ColumnMotion::LineEnd)) {
text[destination..]
.chars()
.next()
.map_or(destination, |character| destination + character.len_utf8())
} else {
destination
};
cursor_byte_index..end
}
const fn is_linewise_motion(motion: Motion) -> bool {
matches!(
motion,
Motion::Down
| Motion::Up
| Motion::LineAddress(
LineAddress::FirstNonBlank | LineAddress::LastNonBlank | LineAddress::Number(_)
)
)
}
fn line_start(text: &str, index: usize) -> usize {
let index = motion::clamp_to_boundary(text, index);
text[..index]
.rfind('\n')
.map_or(0, |newline_index| newline_index + '\n'.len_utf8())
}
fn line_end_including_newline(text: &str, index: usize) -> usize {
let index = motion::clamp_to_boundary(text, index);
let content_end = text[index..]
.find('\n')
.map_or(text.len(), |newline_offset| index + newline_offset);
text[content_end..]
.chars()
.next()
.filter(|character| *character == '\n')
.map_or(content_end, |newline| content_end + newline.len_utf8())
}
pub type ResolvedTarget = OperatorTarget;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ValidatedOperatorRange {
range: ValidatedTextRange,
}
impl ValidatedOperatorRange {
pub fn new(
stream: &TextByteStream,
range: Range<usize>,
) -> Result<Self, TargetResolutionError> {
let range = stream
.validate_range(TextRange::from(range))
.map_err(|error| TargetResolutionError::from_text_stream_error(&error))?;
Ok(Self { range })
}
#[must_use]
pub const fn start(self) -> usize {
self.range.start()
}
#[must_use]
pub const fn end(self) -> usize {
self.range.end()
}
#[must_use]
pub const fn as_range(self) -> Range<usize> {
self.range.as_range()
}
#[must_use]
pub const fn validated_text_range(self) -> ValidatedTextRange {
self.range
}
#[must_use]
pub const fn len(self) -> usize {
self.end() - self.start()
}
#[must_use]
pub const fn is_empty(self) -> bool {
self.start() == self.end()
}
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum TargetResolutionError {
InvertedRange {
start: usize,
end: usize,
},
OutOfBounds {
end: usize,
text_len: usize,
},
InvalidBoundary {
index: usize,
},
UnsupportedTarget,
WrongMode,
NoTextObject,
StaleRange,
}
impl TargetResolutionError {
const fn from_text_stream_error(error: &TextStreamError) -> Self {
match error {
TextStreamError::InvalidUtf8 { .. } => Self::InvalidBoundary { index: 0 },
TextStreamError::OutOfBounds { index, len } => Self::OutOfBounds {
end: *index,
text_len: *len,
},
TextStreamError::NotCharBoundary { index } => Self::InvalidBoundary { index: *index },
TextStreamError::InvalidRange { start, end } => Self::InvertedRange {
start: *start,
end: *end,
},
TextStreamError::StaleValidatedRange { .. }
| TextStreamError::WrongTextStream { .. } => Self::StaleRange,
}
}
}
impl Display for TargetResolutionError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvertedRange { .. } => formatter.write_str("operator target range is inverted"),
Self::OutOfBounds { .. } => {
formatter.write_str("operator target range is out of bounds")
}
Self::InvalidBoundary { .. } => {
formatter.write_str("operator target is not on a UTF-8 boundary")
}
Self::UnsupportedTarget => formatter.write_str("operator target is unsupported"),
Self::WrongMode => formatter.write_str("operator target belongs to a different mode"),
Self::NoTextObject => formatter.write_str("text object target was not found"),
Self::StaleRange => formatter.write_str("operator target range is stale"),
}
}
}
#[cfg(test)]
mod tests {
use super::{OperatorTarget, TargetKind, TargetResolutionError, ValidatedOperatorRange};
use crate::text_stream::TextByteStream;
use crate::vim::{
Counted, Motion, OperatorTargetSource, TextObject, TextObjectKind, TextObjectScope,
VimSelection, VisualMode, WordKind,
};
fn stream(text: &str) -> TextByteStream {
TextByteStream::new(text)
}
#[test]
fn validates_characterwise_utf8_target() {
let stream = stream("aλb");
let target = OperatorTarget::characterwise(&stream, 1.."aλ".len()).unwrap();
assert_eq!(target.kind(), TargetKind::Characterwise);
assert_eq!(target.range().as_range(), 1.."aλ".len());
}
#[test]
fn rejects_invalid_operator_ranges() {
let stream = stream("aλb");
assert_eq!(
ValidatedOperatorRange::new(&stream, std::ops::Range { start: 3, end: 1 }),
Err(TargetResolutionError::InvertedRange { start: 3, end: 1 })
);
assert_eq!(
ValidatedOperatorRange::new(&stream, 0..9),
Err(TargetResolutionError::OutOfBounds {
end: 9,
text_len: stream.as_str().len(),
})
);
assert_eq!(
ValidatedOperatorRange::new(&stream, 2..3),
Err(TargetResolutionError::InvalidBoundary { index: 2 })
);
}
#[test]
fn visual_characterwise_target_includes_cursor_cell() {
let text = "abcd";
let stream = stream(text);
let selection = VimSelection::new(text, 1);
let target =
OperatorTarget::from_visual_selection(&stream, selection, 2, VisualMode::Characterwise)
.unwrap();
assert_eq!(target.kind(), TargetKind::Characterwise);
assert_eq!(target.range().as_range(), 1..3);
}
#[test]
fn visual_linewise_target_includes_trailing_newline_when_present() {
let text = "one\ntwo\nthree";
let stream = stream(text);
let selection = VimSelection::new(text, "one\nt".len());
let target = OperatorTarget::from_visual_selection(
&stream,
selection,
"one\ntwo".len(),
VisualMode::Linewise,
)
.unwrap();
assert_eq!(target.kind(), TargetKind::Linewise);
assert_eq!(target.range().as_range(), 4..8);
}
#[test]
fn visual_blockwise_operator_target_fails_closed_until_block_edits_exist() {
let text = "one\ntwo";
let stream = stream(text);
let selection = VimSelection::new(text, 1);
assert_eq!(
OperatorTarget::from_visual_selection(
&stream,
selection,
"one\nt".len(),
VisualMode::Blockwise
),
Err(TargetResolutionError::UnsupportedTarget)
);
}
#[test]
fn normal_word_motion_resolves_characterwise_range() {
let stream = stream("one two three");
let target = OperatorTarget::from_normal_source(
&stream,
0,
OperatorTargetSource::Motion(Counted::once(Motion::WordForward(WordKind::Normal))),
)
.unwrap();
assert_eq!(target.kind(), TargetKind::Characterwise);
assert_eq!(target.range().as_range(), 0.."one ".len());
}
#[test]
fn normal_current_line_resolves_linewise_range() {
let text = "one\ntwo\nthree";
let stream = stream(text);
let target = OperatorTarget::from_normal_source(
&stream,
"one\nt".len(),
OperatorTargetSource::CurrentLine {
count: crate::vim::Count::default(),
},
)
.unwrap();
assert_eq!(target.kind(), TargetKind::Linewise);
assert_eq!(target.range().as_range(), 4..8);
}
#[test]
fn vertical_motion_resolves_linewise_range() {
let stream = stream("one\ntwo\nthree");
let target = OperatorTarget::from_normal_source(
&stream,
0,
OperatorTargetSource::Motion(Counted::once(Motion::Down)),
)
.unwrap();
assert_eq!(target.kind(), TargetKind::Linewise);
assert_eq!(target.range().as_range(), 0..8);
}
#[test]
fn normal_text_object_resolves_characterwise_range() {
let text = "one two\n";
let stream = stream(text);
let target = OperatorTarget::from_normal_source(
&stream,
"one ".len(),
OperatorTargetSource::TextObject(Counted::once(TextObject::new(
TextObjectScope::Inner,
TextObjectKind::Word,
))),
)
.unwrap();
assert_eq!(target.kind(), TargetKind::Characterwise);
assert_eq!(target.range().as_range(), "one ".len().."one two".len());
}
}