use std::iter;
use std::ops::Range;
pub trait WrapPolicy {
fn wrap_logical_line(line: &str, width: u16) -> Vec<VisualLine>;
}
pub trait TextLayout {
type Wrapping: WrapPolicy;
fn text(&self) -> &str;
fn lines(&self, width: u16) -> Vec<VisualLine> {
wrap::<Self::Wrapping>(self.text(), width)
}
}
pub struct VisualLine {
pub range: Range<usize>,
pub hard_continuation: bool,
}
struct Projection {
source: Range<usize>,
wrapped: Range<usize>,
}
pub struct WrappedText {
text: String,
projections: Vec<Projection>,
hard_continuations: Vec<usize>,
}
impl WrappedText {
pub fn word(source: &str, width: u16) -> Self {
Self::from_lines(source, wrap::<WordWrap>(source, width))
}
pub fn code(source: &str, width: u16, token_ends: &[usize]) -> Self {
Self::from_lines(source, wrap_at_tokens(source, width, token_ends))
}
fn from_lines(source: &str, lines: Vec<VisualLine>) -> Self {
let mut text = String::new();
let mut projections = Vec::with_capacity(lines.len());
let mut hard_continuations = Vec::new();
for (index, line) in lines.into_iter().enumerate() {
if index > 0 {
text.push('\n');
}
let start = text.len();
if line.hard_continuation {
hard_continuations.push(start);
}
text.push_str(&source[line.range.clone()]);
projections.push(Projection {
source: line.range,
wrapped: start..text.len(),
});
}
Self {
text,
projections,
hard_continuations,
}
}
pub fn text(&self) -> &str {
&self.text
}
pub fn height(&self) -> u16 {
self.text.matches('\n').count() as u16 + 1
}
pub fn hard_continuations(&self) -> impl Iterator<Item = usize> + '_ {
self.hard_continuations.iter().copied()
}
pub fn relocate(&self, range: &Range<usize>) -> Vec<Range<usize>> {
self.projections
.iter()
.filter_map(|projection| {
let start = range.start.max(projection.source.start);
let end = range.end.min(projection.source.end);
(start < end).then(|| {
projection.wrapped.start + start - projection.source.start
..projection.wrapped.start + end - projection.source.start
})
})
.collect()
}
}
pub struct HardWrap;
impl WrapPolicy for HardWrap {
fn wrap_logical_line(line: &str, width: u16) -> Vec<VisualLine> {
let width = width as usize;
if width == 0 || line.is_empty() {
return iter::once(VisualLine {
range: 0..line.len(),
hard_continuation: false,
})
.collect();
}
let mut rows = Vec::new();
let mut start = 0;
let mut columns = 0;
let mut hard_continuation = false;
for (byte, _) in line.char_indices() {
if columns == width {
rows.push(VisualLine {
range: start..byte,
hard_continuation,
});
start = byte;
columns = 0;
hard_continuation = true;
}
columns += 1;
}
rows.push(VisualLine {
range: start..line.len(),
hard_continuation,
});
if columns == width {
rows.push(VisualLine {
range: line.len()..line.len(),
hard_continuation: true,
});
}
rows
}
}
pub struct WordWrap;
impl WrapPolicy for WordWrap {
fn wrap_logical_line(line: &str, width: u16) -> Vec<VisualLine> {
let width = width as usize;
if width == 0 || line.is_empty() {
return iter::once(VisualLine {
range: 0..line.len(),
hard_continuation: false,
})
.collect();
}
let characters: Vec<(usize, char)> = line.char_indices().collect();
let count = characters.len();
let byte_at = |index: usize| characters.get(index).map_or(line.len(), |&(byte, _)| byte);
let mut rows = Vec::new();
let mut start = 0;
let mut hard_continuation = false;
while start < count {
if count - start <= width {
rows.push(VisualLine {
range: byte_at(start)..line.len(),
hard_continuation,
});
break;
}
let hard_end = start + width;
let (end, next, hard_break) = if let Some(index) = (start + 1..hard_end)
.rev()
.find(|&index| characters[index].1 == ' ')
{
(index, index + 1, false)
} else if let Some(index) = (start + 1..hard_end)
.rev()
.find(|&index| !is_word_character(characters[index].1))
{
(index, index, false)
} else {
(hard_end, hard_end, true)
};
rows.push(VisualLine {
range: byte_at(start)..byte_at(end),
hard_continuation,
});
start = next;
hard_continuation = hard_break;
}
rows
}
}
pub fn wrap<Policy: WrapPolicy>(text: &str, width: u16) -> Vec<VisualLine> {
let mut lines = Vec::new();
let mut logical_start = 0;
for logical in text.split('\n') {
lines.extend(
Policy::wrap_logical_line(logical, width)
.into_iter()
.map(|line| VisualLine {
range: logical_start + line.range.start..logical_start + line.range.end,
hard_continuation: line.hard_continuation,
}),
);
logical_start += logical.len() + 1;
}
lines
}
pub fn wrap_at_tokens(text: &str, width: u16, token_ends: &[usize]) -> Vec<VisualLine> {
let width = width as usize;
let mut lines = Vec::new();
let mut logical_start = 0;
for logical in text.split('\n') {
let characters: Vec<(usize, char)> = logical.char_indices().collect();
let byte_at = |index: usize| {
characters
.get(index)
.map_or(logical.len(), |&(byte, _)| byte)
};
let mut start = 0;
let mut hard_continuation = false;
if width == 0 || characters.is_empty() {
lines.push(VisualLine {
range: logical_start..logical_start + logical.len(),
hard_continuation: false,
});
logical_start += logical.len() + 1;
continue;
}
while characters.len() - start > width {
let limit = start + width;
let whitespace = (start + 1..=limit)
.rev()
.find(|&index| characters[index].1.is_whitespace());
if let Some(end) = whitespace {
lines.push(VisualLine {
range: logical_start + byte_at(start)..logical_start + byte_at(end),
hard_continuation,
});
start = end + 1;
hard_continuation = false;
continue;
}
let start_byte = logical_start + byte_at(start);
let limit_byte = logical_start + byte_at(limit);
if let Some(&end) = token_ends
.iter()
.rev()
.find(|&&end| start_byte < end && end <= limit_byte)
{
lines.push(VisualLine {
range: start_byte..end,
hard_continuation,
});
start = characters.partition_point(|&(byte, _)| logical_start + byte < end);
hard_continuation = false;
continue;
}
lines.push(VisualLine {
range: start_byte..limit_byte,
hard_continuation,
});
start = limit;
hard_continuation = true;
}
lines.push(VisualLine {
range: logical_start + byte_at(start)..logical_start + logical.len(),
hard_continuation,
});
logical_start += logical.len() + 1;
}
lines
}
fn is_word_character(character: char) -> bool {
character.is_alphanumeric() || character == '\'' || character == '$'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn token_wrapping_prefers_whitespace_then_token_boundaries() {
let whitespace = wrap_at_tokens("aa bbbb", 4, &[2, 7]);
assert_eq!(whitespace[0].range, 0..2);
assert_eq!(whitespace[1].range, 3..7);
assert!(!whitespace[1].hard_continuation);
let tokens = wrap_at_tokens("abc+def", 5, &[3, 4, 7]);
assert_eq!(tokens[0].range, 0..4);
assert_eq!(tokens[1].range, 4..7);
assert!(!tokens[1].hard_continuation);
}
#[test]
fn a_single_long_token_marks_its_continuation() {
let lines = wrap_at_tokens("abcdefgh", 4, &[8]);
assert_eq!(lines[0].range, 0..4);
assert!(!lines[0].hard_continuation);
assert_eq!(lines[1].range, 4..8);
assert!(lines[1].hard_continuation);
}
#[test]
fn shared_wrapper_reports_hard_word_continuations() {
let wrapped = WrappedText::word("abcdefgh", 4);
assert_eq!(wrapped.text(), "abcd\nefgh");
assert_eq!(wrapped.hard_continuations().collect::<Vec<_>>(), [5]);
}
}