use std::iter;
use std::ops::Range;
pub struct VisualLine {
pub range: Range<usize>,
pub hard_continuation: bool,
}
impl VisualLine {
fn shifted(mut self, offset: usize) -> Self {
self.range = offset + self.range.start..offset + self.range.end;
self
}
}
pub trait WrapPolicy {
fn wrap_logical_line(&self, line: &str, width: u16, offset: usize) -> Vec<VisualLine>;
fn wrap(&self, text: &str, width: u16) -> Vec<VisualLine> {
let mut lines = Vec::new();
let mut offset = 0;
for logical in text.split('\n') {
lines.extend(self.wrap_logical_line(logical, width, offset));
offset += logical.len() + 1;
}
lines
}
}
pub trait TextLayout {
type Wrapping: Default + WrapPolicy;
fn text(&self) -> &str;
fn lines(&self, width: u16) -> Vec<VisualLine> {
Self::Wrapping::default().wrap(self.text(), width)
}
}
struct Projection {
source: Range<usize>,
hard_continuation: bool,
}
pub struct WrappedText {
text: String,
projections: Vec<Projection>,
}
impl WrappedText {
pub fn word(source: &str, width: u16) -> Self {
Self::from_lines(source, WordWrap.wrap(source, width))
}
pub fn code(source: &str, width: u16, spans: &[Range<usize>]) -> Self {
Self::from_lines(source, SpanWrap(spans).wrap(source, width))
}
fn from_lines(source: &str, lines: Vec<VisualLine>) -> Self {
let mut text = String::new();
let mut projections = Vec::with_capacity(lines.len());
for (index, line) in lines.into_iter().enumerate() {
if index > 0 {
text.push('\n');
}
text.push_str(&source[line.range.clone()]);
projections.push(Projection {
source: line.range,
hard_continuation: line.hard_continuation,
});
}
Self { text, projections }
}
pub fn text(&self) -> &str {
&self.text
}
pub fn height(&self) -> u16 {
self.text.matches('\n').count() as u16 + 1
}
pub fn source_lines(&self) -> impl Iterator<Item = (&Range<usize>, bool)> {
self.projections
.iter()
.map(|projection| (&projection.source, projection.hard_continuation))
}
}
#[derive(Default)]
pub struct HardWrap;
impl WrapPolicy for HardWrap {
fn wrap_logical_line(&self, line: &str, width: u16, offset: usize) -> Vec<VisualLine> {
let width = width as usize;
if width == 0 || line.is_empty() {
return iter::once(VisualLine {
range: 0..line.len(),
hard_continuation: false,
})
.map(|line| line.shifted(offset))
.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.into_iter().map(|line| line.shifted(offset)).collect()
}
}
#[derive(Default)]
struct WordWrap;
impl WrapPolicy for WordWrap {
fn wrap_logical_line(&self, line: &str, width: u16, offset: usize) -> Vec<VisualLine> {
let width = width as usize;
if width == 0 || line.is_empty() {
return iter::once(VisualLine {
range: 0..line.len(),
hard_continuation: false,
})
.map(|line| line.shifted(offset))
.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.into_iter().map(|line| line.shifted(offset)).collect()
}
}
struct SpanWrap<'a>(pub &'a [Range<usize>]);
impl WrapPolicy for SpanWrap<'_> {
fn wrap_logical_line(
&self,
logical: &str,
width: u16,
logical_start: usize,
) -> Vec<VisualLine> {
let width = width as usize;
let mut lines = Vec::new();
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,
});
return lines;
}
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) = self
.0
.iter()
.rev()
.map(|span| span.end)
.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,
});
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 = SpanWrap(&[0..2, 3..7]).wrap("aa bbbb", 4);
assert_eq!(whitespace[0].range, 0..2);
assert_eq!(whitespace[1].range, 3..7);
assert!(!whitespace[1].hard_continuation);
let tokens = SpanWrap(&[0..3, 3..4, 4..7]).wrap("abc+def", 5);
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 span = 0..8;
let lines = SpanWrap(std::slice::from_ref(&span)).wrap("abcdefgh", 4);
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
.source_lines()
.filter_map(|(range, hard)| hard.then_some(range.start))
.collect::<Vec<_>>(),
[4]
);
}
}