use std::{
cell::{Cell, RefCell},
cmp::Ordering,
collections::HashMap,
rc::Rc,
sync::Arc,
};
use floem_editor_core::{
buffer::rope_text::{RopeText, RopeTextVal},
cursor::CursorAffinity,
word::WordCursor,
};
use floem_reactive::Scope;
use floem_renderer::text::{HitPosition, LayoutGlyph, TextLayout};
use lapce_xi_rope::{Interval, Rope};
use peniko::kurbo::Point;
use super::{layout::TextLayoutLine, listener::Listener};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ResolvedWrap {
None,
Column(usize),
Width(f32),
}
impl ResolvedWrap {
pub fn is_different_kind(self, other: ResolvedWrap) -> bool {
!matches!(
(self, other),
(ResolvedWrap::None, ResolvedWrap::None)
| (ResolvedWrap::Column(_), ResolvedWrap::Column(_))
| (ResolvedWrap::Width(_), ResolvedWrap::Width(_))
)
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct VLine(pub usize);
impl VLine {
pub fn get(&self) -> usize {
self.0
}
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct RVLine {
pub line: usize,
pub line_index: usize,
}
impl RVLine {
pub fn new(line: usize, line_index: usize) -> RVLine {
RVLine { line, line_index }
}
pub fn is_first(&self) -> bool {
self.line_index == 0
}
}
pub type Layouts = HashMap<usize, HashMap<usize, Arc<TextLayoutLine>>>;
#[derive(Debug, Default, PartialEq, Clone, Copy)]
pub struct ConfigId {
editor_style_id: u64,
floem_style_id: u64,
}
impl ConfigId {
pub fn new(editor_style_id: u64, floem_style_id: u64) -> Self {
Self {
editor_style_id,
floem_style_id,
}
}
}
#[derive(Default)]
pub struct TextLayoutCache {
config_id: ConfigId,
cache_rev: u64,
pub layouts: Layouts,
pub max_width: f64,
}
impl TextLayoutCache {
pub fn clear(&mut self, cache_rev: u64, config_id: Option<ConfigId>) {
self.layouts.clear();
if let Some(config_id) = config_id {
self.config_id = config_id;
}
self.cache_rev = cache_rev;
self.max_width = 0.0;
}
pub fn clear_unchanged(&mut self) {
self.layouts.clear();
self.max_width = 0.0;
}
pub fn get(&self, font_size: usize, line: usize) -> Option<&Arc<TextLayoutLine>> {
self.layouts.get(&font_size).and_then(|c| c.get(&line))
}
pub fn get_mut(&mut self, font_size: usize, line: usize) -> Option<&mut Arc<TextLayoutLine>> {
self.layouts
.get_mut(&font_size)
.and_then(|c| c.get_mut(&line))
}
pub fn get_layout_col(
&self,
text_prov: &impl TextLayoutProvider,
font_size: usize,
line: usize,
line_index: usize,
) -> Option<(usize, usize)> {
self.get(font_size, line)
.and_then(|l| l.layout_cols(text_prov, line).nth(line_index))
}
}
pub trait TextLayoutProvider {
fn text(&self) -> Rope;
fn rope_text(&self) -> RopeTextVal {
RopeTextVal::new(self.text())
}
fn new_text_layout(
&self,
line: usize,
font_size: usize,
wrap: ResolvedWrap,
) -> Arc<TextLayoutLine>;
fn before_phantom_col(&self, line: usize, col: usize) -> usize;
fn has_multiline_phantom(&self) -> bool;
}
impl<T: TextLayoutProvider> TextLayoutProvider for &T {
fn text(&self) -> Rope {
(**self).text()
}
fn new_text_layout(
&self,
line: usize,
font_size: usize,
wrap: ResolvedWrap,
) -> Arc<TextLayoutLine> {
(**self).new_text_layout(line, font_size, wrap)
}
fn before_phantom_col(&self, line: usize, col: usize) -> usize {
(**self).before_phantom_col(line, col)
}
fn has_multiline_phantom(&self) -> bool {
(**self).has_multiline_phantom()
}
}
pub type FontSizeCacheId = u64;
pub trait LineFontSizeProvider {
fn font_size(&self, line: usize) -> usize;
fn cache_id(&self) -> FontSizeCacheId;
}
#[derive(Debug, Clone, PartialEq)]
pub enum LayoutEvent {
CreatedLayout { font_size: usize, line: usize },
}
pub struct Lines {
pub font_sizes: RefCell<Rc<dyn LineFontSizeProvider>>,
text_layouts: Rc<RefCell<TextLayoutCache>>,
wrap: Cell<ResolvedWrap>,
font_size_cache_id: Cell<FontSizeCacheId>,
last_vline: Rc<Cell<Option<VLine>>>,
pub layout_event: Listener<LayoutEvent>,
}
impl Lines {
pub fn new(cx: Scope, font_sizes: RefCell<Rc<dyn LineFontSizeProvider>>) -> Lines {
let id = font_sizes.borrow().cache_id();
Lines {
font_sizes,
text_layouts: Rc::new(RefCell::new(TextLayoutCache::default())),
wrap: Cell::new(ResolvedWrap::None),
font_size_cache_id: Cell::new(id),
last_vline: Rc::new(Cell::new(None)),
layout_event: Listener::new_empty(cx),
}
}
pub fn wrap(&self) -> ResolvedWrap {
self.wrap.get()
}
pub fn set_wrap(&self, wrap: ResolvedWrap) {
if wrap == self.wrap.get() {
return;
}
self.clear_unchanged();
self.wrap.set(wrap);
}
pub fn max_width(&self) -> f64 {
self.text_layouts.borrow().max_width
}
pub fn is_linear(&self, text_prov: impl TextLayoutProvider) -> bool {
self.wrap.get() == ResolvedWrap::None && !text_prov.has_multiline_phantom()
}
pub fn font_size(&self, line: usize) -> usize {
self.font_sizes.borrow().font_size(line)
}
pub fn last_vline(&self, text_prov: impl TextLayoutProvider) -> VLine {
let current_id = self.font_sizes.borrow().cache_id();
if current_id != self.font_size_cache_id.get() {
self.last_vline.set(None);
self.font_size_cache_id.set(current_id);
}
if let Some(last_vline) = self.last_vline.get() {
last_vline
} else {
let rope_text = text_prov.rope_text();
let hard_line_count = rope_text.num_lines();
let line_count = if self.is_linear(text_prov) {
hard_line_count
} else {
let mut soft_line_count = 0;
let layouts = self.text_layouts.borrow();
for i in 0..hard_line_count {
let font_size = self.font_size(i);
if let Some(text_layout) = layouts.get(font_size, i) {
let line_count = text_layout.line_count();
soft_line_count += line_count;
} else {
soft_line_count += 1;
}
}
soft_line_count
};
let last_vline = line_count.saturating_sub(1);
self.last_vline.set(Some(VLine(last_vline)));
VLine(last_vline)
}
}
pub fn clear_last_vline(&self) {
self.last_vline.set(None);
}
pub fn last_rvline(&self, text_prov: impl TextLayoutProvider) -> RVLine {
let rope_text = text_prov.rope_text();
let last_line = rope_text.last_line();
let layouts = self.text_layouts.borrow();
let font_size = self.font_size(last_line);
if let Some(layout) = layouts.get(font_size, last_line) {
let line_count = layout.line_count();
RVLine::new(last_line, line_count - 1)
} else {
RVLine::new(last_line, 0)
}
}
pub fn num_vlines(&self, text_prov: impl TextLayoutProvider) -> usize {
self.last_vline(text_prov).get() + 1
}
pub fn get_init_text_layout(
&self,
cache_rev: u64,
config_id: ConfigId,
text_prov: impl TextLayoutProvider,
line: usize,
trigger: bool,
) -> Arc<TextLayoutLine> {
self.check_cache(cache_rev, config_id);
let font_size = self.font_size(line);
get_init_text_layout(
&self.text_layouts,
trigger.then_some(self.layout_event),
text_prov,
line,
font_size,
self.wrap.get(),
&self.last_vline,
)
}
pub fn try_get_text_layout(
&self,
cache_rev: u64,
config_id: ConfigId,
line: usize,
) -> Option<Arc<TextLayoutLine>> {
self.check_cache(cache_rev, config_id);
let font_size = self.font_size(line);
self.text_layouts
.borrow()
.layouts
.get(&font_size)
.and_then(|f| f.get(&line))
.cloned()
}
pub fn init_line_interval(
&self,
cache_rev: u64,
config_id: ConfigId,
text_prov: &impl TextLayoutProvider,
lines: impl Iterator<Item = usize>,
trigger: bool,
) {
for line in lines {
self.get_init_text_layout(cache_rev, config_id, text_prov, line, trigger);
}
}
pub fn init_all(
&self,
cache_rev: u64,
config_id: ConfigId,
text_prov: &impl TextLayoutProvider,
trigger: bool,
) {
let text = text_prov.text();
let last_line = text.line_of_offset(text.len());
self.init_line_interval(cache_rev, config_id, text_prov, 0..=last_line, trigger);
}
pub fn iter_vlines(
&self,
text_prov: impl TextLayoutProvider,
backwards: bool,
start: VLine,
) -> impl Iterator<Item = VLineInfo> {
VisualLines::new(self, text_prov, backwards, start)
}
pub fn iter_vlines_over(
&self,
text_prov: impl TextLayoutProvider,
backwards: bool,
start: VLine,
end: VLine,
) -> impl Iterator<Item = VLineInfo> {
self.iter_vlines(text_prov, backwards, start)
.take_while(move |info| info.vline < end)
}
pub fn iter_rvlines(
&self,
text_prov: impl TextLayoutProvider,
backwards: bool,
start: RVLine,
) -> impl Iterator<Item = VLineInfo<()>> {
VisualLinesRelative::new(self, text_prov, backwards, start)
}
pub fn iter_rvlines_over(
&self,
text_prov: impl TextLayoutProvider,
backwards: bool,
start: RVLine,
end_line: usize,
) -> impl Iterator<Item = VLineInfo<()>> {
self.iter_rvlines(text_prov, backwards, start)
.take_while(move |info| info.rvline.line < end_line)
}
pub fn iter_vlines_init(
&self,
text_prov: impl TextLayoutProvider + Clone,
cache_rev: u64,
config_id: ConfigId,
start: VLine,
trigger: bool,
) -> impl Iterator<Item = VLineInfo> {
self.check_cache(cache_rev, config_id);
if start <= self.last_vline(&text_prov) {
let (_, rvline) = find_vline_init_info(self, &text_prov, start).unwrap();
self.get_init_text_layout(cache_rev, config_id, &text_prov, rvline.line, trigger);
}
let text_layouts = self.text_layouts.clone();
let font_sizes = self.font_sizes.clone();
let wrap = self.wrap.get();
let last_vline = self.last_vline.clone();
let layout_event = trigger.then_some(self.layout_event);
self.iter_vlines(text_prov.clone(), false, start)
.inspect(move |v| {
if v.is_first() {
let next_line = v.rvline.line + 1;
let font_size = font_sizes.borrow().font_size(next_line);
get_init_text_layout(
&text_layouts,
layout_event,
&text_prov,
next_line,
font_size,
wrap,
&last_vline,
);
}
})
}
pub fn iter_vlines_init_over(
&self,
text_prov: impl TextLayoutProvider + Clone,
cache_rev: u64,
config_id: ConfigId,
start: VLine,
end: VLine,
trigger: bool,
) -> impl Iterator<Item = VLineInfo> {
self.iter_vlines_init(text_prov, cache_rev, config_id, start, trigger)
.take_while(move |info| info.vline < end)
}
pub fn iter_rvlines_init(
&self,
text_prov: impl TextLayoutProvider + Clone,
cache_rev: u64,
config_id: ConfigId,
start: RVLine,
trigger: bool,
) -> impl Iterator<Item = VLineInfo<()>> {
self.check_cache(cache_rev, config_id);
if start.line <= text_prov.rope_text().last_line() {
self.get_init_text_layout(cache_rev, config_id, &text_prov, start.line, trigger);
}
let text_layouts = self.text_layouts.clone();
let font_sizes = self.font_sizes.clone();
let wrap = self.wrap.get();
let last_vline = self.last_vline.clone();
let layout_event = trigger.then_some(self.layout_event);
self.iter_rvlines(text_prov.clone(), false, start)
.inspect(move |v| {
if v.is_first() {
let next_line = v.rvline.line + 1;
let font_size = font_sizes.borrow().font_size(next_line);
get_init_text_layout(
&text_layouts,
layout_event,
&text_prov,
next_line,
font_size,
wrap,
&last_vline,
);
}
})
}
pub fn vline_of_offset(
&self,
text_prov: &impl TextLayoutProvider,
offset: usize,
affinity: CursorAffinity,
) -> VLine {
let text = text_prov.text();
let offset = offset.min(text.len());
if self.is_linear(text_prov) {
let buffer_line = text.line_of_offset(offset);
return VLine(buffer_line);
}
let Some((vline, _line_index)) = find_vline_of_offset(self, text_prov, offset, affinity)
else {
return self.last_vline(text_prov);
};
vline
}
pub fn vline_col_of_offset(
&self,
text_prov: &impl TextLayoutProvider,
offset: usize,
affinity: CursorAffinity,
) -> (VLine, usize) {
let vline = self.vline_of_offset(text_prov, offset, affinity);
let last_col = self
.iter_vlines(text_prov, false, vline)
.next()
.map(|info| info.last_col(text_prov, true))
.unwrap_or(0);
let line = text_prov.text().line_of_offset(offset);
let line_offset = text_prov.text().offset_of_line(line);
let col = offset - line_offset;
let col = col.min(last_col);
(vline, col)
}
pub fn offset_of_vline(&self, text_prov: &impl TextLayoutProvider, vline: VLine) -> usize {
find_vline_init_info(self, text_prov, vline)
.map(|x| x.0)
.unwrap_or_else(|| text_prov.text().len())
}
pub fn vline_of_line(&self, text_prov: &impl TextLayoutProvider, line: usize) -> VLine {
if self.is_linear(text_prov) {
return VLine(line);
}
find_vline_of_line(self, text_prov, line).unwrap_or_else(|| self.last_vline(text_prov))
}
pub fn vline_of_rvline(&self, text_prov: &impl TextLayoutProvider, rvline: RVLine) -> VLine {
if self.is_linear(text_prov) {
debug_assert_eq!(
rvline.line_index, 0,
"Got a nonzero line index despite being linear, old RVLine was used."
);
return VLine(rvline.line);
}
let vline = self.vline_of_line(text_prov, rvline.line);
VLine(vline.get() + rvline.line_index)
}
pub fn rvline_of_offset(
&self,
text_prov: &impl TextLayoutProvider,
offset: usize,
affinity: CursorAffinity,
) -> RVLine {
let text = text_prov.text();
let offset = offset.min(text.len());
if self.is_linear(text_prov) {
let buffer_line = text.line_of_offset(offset);
return RVLine::new(buffer_line, 0);
}
find_rvline_of_offset(self, text_prov, offset, affinity)
.unwrap_or_else(|| self.last_rvline(text_prov))
}
pub fn rvline_col_of_offset(
&self,
text_prov: &impl TextLayoutProvider,
offset: usize,
affinity: CursorAffinity,
) -> (RVLine, usize) {
let rvline = self.rvline_of_offset(text_prov, offset, affinity);
let info = self.iter_rvlines(text_prov, false, rvline).next().unwrap();
let line_offset = text_prov.text().offset_of_line(rvline.line);
let col = offset - line_offset;
let col = col.min(info.last_col(text_prov, true));
(rvline, col)
}
pub fn offset_of_rvline(
&self,
text_prov: &impl TextLayoutProvider,
RVLine { line, line_index }: RVLine,
) -> usize {
let rope_text = text_prov.rope_text();
let font_size = self.font_size(line);
let layouts = self.text_layouts.borrow();
if let Some(text_layout) = layouts.get(font_size, line) {
debug_assert!(
line_index < text_layout.line_count(),
"Line index was out of bounds. This likely indicates keeping an rvline past when it was valid."
);
let line_index = line_index.min(text_layout.line_count() - 1);
let col = text_layout
.start_layout_cols(text_prov, line)
.nth(line_index)
.unwrap_or(0);
let col = text_prov.before_phantom_col(line, col);
rope_text.offset_of_line_col(line, col)
} else {
debug_assert_eq!(line_index, 0, "Line index was zero. This likely indicates keeping an rvline past when it was valid.");
rope_text.offset_of_line(line)
}
}
pub fn rvline_of_line(&self, text_prov: &impl TextLayoutProvider, line: usize) -> RVLine {
if self.is_linear(text_prov) {
return RVLine::new(line, 0);
}
let offset = text_prov.rope_text().offset_of_line(line);
find_rvline_of_offset(self, text_prov, offset, CursorAffinity::Backward)
.unwrap_or_else(|| self.last_rvline(text_prov))
}
pub fn check_cache(&self, cache_rev: u64, config_id: ConfigId) {
let (prev_cache_rev, prev_config_id) = {
let l = self.text_layouts.borrow();
(l.cache_rev, l.config_id)
};
if cache_rev != prev_cache_rev || config_id != prev_config_id {
self.clear(cache_rev, Some(config_id));
}
}
pub fn check_cache_rev(&self, cache_rev: u64) {
if cache_rev != self.text_layouts.borrow().cache_rev {
self.clear(cache_rev, None);
}
}
pub fn clear(&self, cache_rev: u64, config_id: Option<ConfigId>) {
self.text_layouts.borrow_mut().clear(cache_rev, config_id);
self.last_vline.set(None);
}
pub fn clear_unchanged(&self) {
self.text_layouts.borrow_mut().clear_unchanged();
self.last_vline.set(None);
}
}
fn get_init_text_layout(
text_layouts: &RefCell<TextLayoutCache>,
layout_event: Option<Listener<LayoutEvent>>,
text_prov: impl TextLayoutProvider,
line: usize,
font_size: usize,
wrap: ResolvedWrap,
last_vline: &Cell<Option<VLine>>,
) -> Arc<TextLayoutLine> {
if !text_layouts.borrow().layouts.contains_key(&font_size) {
let mut cache = text_layouts.borrow_mut();
cache.layouts.insert(font_size, HashMap::new());
}
let cache_exists = text_layouts
.borrow()
.layouts
.get(&font_size)
.unwrap()
.get(&line)
.is_some();
if !cache_exists {
let text_layout = text_prov.new_text_layout(line, font_size, wrap);
if let Some(vline) = last_vline.get() {
let last_line = text_prov.rope_text().last_line();
if line <= last_line {
let vline = vline.get();
let new_vline = vline + (text_layout.line_count() - 1);
last_vline.set(Some(VLine(new_vline)));
}
}
{
let mut cache = text_layouts.borrow_mut();
let width = text_layout.text.size().width;
if width > cache.max_width {
cache.max_width = width;
}
cache
.layouts
.get_mut(&font_size)
.unwrap()
.insert(line, text_layout);
}
if let Some(layout_event) = layout_event {
layout_event.send(LayoutEvent::CreatedLayout { font_size, line });
}
}
text_layouts
.borrow()
.layouts
.get(&font_size)
.unwrap()
.get(&line)
.cloned()
.unwrap()
}
fn find_vline_of_offset(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
offset: usize,
affinity: CursorAffinity,
) -> Option<(VLine, usize)> {
let layouts = lines.text_layouts.borrow();
let rope_text = text_prov.rope_text();
let buffer_line = rope_text.line_of_offset(offset);
let line_start_offset = rope_text.offset_of_line(buffer_line);
let vline = find_vline_of_line(lines, text_prov, buffer_line)?;
let font_size = lines.font_size(buffer_line);
let Some(text_layout) = layouts.get(font_size, buffer_line) else {
return Some((vline, 0));
};
let col = offset - line_start_offset;
let (vline, line_index) = find_start_line_index(text_prov, text_layout, buffer_line, col)
.map(|line_index| (VLine(vline.get() + line_index), line_index))?;
if line_index > 0 {
if let CursorAffinity::Backward = affinity {
let line_end = lines.offset_of_vline(text_prov, vline);
if line_end == offset && vline.get() != 0 {
return Some((VLine(vline.get() - 1), line_index - 1));
}
}
}
Some((vline, line_index))
}
fn find_rvline_of_offset(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
offset: usize,
affinity: CursorAffinity,
) -> Option<RVLine> {
let layouts = lines.text_layouts.borrow();
let rope_text = text_prov.rope_text();
let buffer_line = rope_text.line_of_offset(offset);
let line_start_offset = rope_text.offset_of_line(buffer_line);
let font_size = lines.font_size(buffer_line);
let Some(text_layout) = layouts.get(font_size, buffer_line) else {
return Some(RVLine::new(buffer_line, 0));
};
let col = offset - line_start_offset;
let rv = find_start_line_index(text_prov, text_layout, buffer_line, col)
.map(|line_index| RVLine::new(buffer_line, line_index))?;
if rv.line_index > 0 {
if let CursorAffinity::Backward = affinity {
let line_end = lines.offset_of_rvline(text_prov, rv);
if line_end == offset {
if rv.line_index > 0 {
return Some(RVLine::new(rv.line, rv.line_index - 1));
} else if rv.line == 0 {
} else {
let font_sizes = lines.font_sizes.borrow();
let (prev, _) = prev_rvline(&layouts, text_prov, &**font_sizes, rv)?;
return Some(prev);
}
}
}
}
Some(rv)
}
fn find_start_line_index(
text_prov: &impl TextLayoutProvider,
text_layout: &TextLayoutLine,
line: usize,
col: usize,
) -> Option<usize> {
let mut starts = text_layout
.layout_cols(text_prov, line)
.enumerate()
.peekable();
while let Some((i, (layout_start, _))) = starts.next() {
let layout_start = text_prov.before_phantom_col(line, layout_start);
if layout_start >= col {
return Some(i);
}
let next_start = starts
.peek()
.map(|(_, (next_start, _))| text_prov.before_phantom_col(line, *next_start));
if let Some(next_start) = next_start {
if next_start > col {
return Some(i);
}
} else {
return Some(i);
}
}
None
}
fn find_vline_of_line(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
line: usize,
) -> Option<VLine> {
let rope = text_prov.rope_text();
let last_line = rope.last_line();
if line > last_line / 2 {
let last_vline = lines.last_vline(text_prov);
let last_rvline = lines.last_rvline(text_prov);
let last_start_vline = VLine(last_vline.get() - last_rvline.line_index);
find_vline_of_line_backwards(lines, (last_start_vline, last_line), line)
} else {
find_vline_of_line_forwards(lines, (VLine(0), 0), line)
}
}
fn find_vline_of_line_backwards(
lines: &Lines,
(start, s_line): (VLine, usize),
line: usize,
) -> Option<VLine> {
if line > s_line {
return None;
} else if line == s_line {
return Some(start);
} else if line == 0 {
return Some(VLine(0));
}
let layouts = lines.text_layouts.borrow();
let mut cur_vline = start.get();
for cur_line in line..s_line {
let font_size = lines.font_size(cur_line);
let Some(text_layout) = layouts.get(font_size, cur_line) else {
cur_vline -= 1;
continue;
};
let line_count = text_layout.line_count();
cur_vline -= line_count;
}
Some(VLine(cur_vline))
}
fn find_vline_of_line_forwards(
lines: &Lines,
(start, s_line): (VLine, usize),
line: usize,
) -> Option<VLine> {
match line.cmp(&s_line) {
Ordering::Equal => return Some(start),
Ordering::Less => return None,
Ordering::Greater => (),
}
let layouts = lines.text_layouts.borrow();
let mut cur_vline = start.get();
for cur_line in s_line..line {
let font_size = lines.font_size(cur_line);
let Some(text_layout) = layouts.get(font_size, cur_line) else {
cur_vline += 1;
continue;
};
let line_count = text_layout.line_count();
cur_vline += line_count;
}
Some(VLine(cur_vline))
}
fn find_vline_init_info(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
vline: VLine,
) -> Option<(usize, RVLine)> {
let rope_text = text_prov.rope_text();
if vline.get() == 0 {
return Some((0, RVLine::new(0, 0)));
}
if lines.is_linear(text_prov) {
let line = vline.get();
if line > rope_text.last_line() {
return None;
}
return Some((rope_text.offset_of_line(line), RVLine::new(line, 0)));
}
let last_vline = lines.last_vline(text_prov);
if vline > last_vline {
return None;
}
if vline.get() < last_vline.get() / 2 {
let last_rvline = lines.last_rvline(text_prov);
find_vline_init_info_rv_backward(lines, text_prov, (last_vline, last_rvline), vline)
} else {
find_vline_init_info_forward(lines, text_prov, (VLine(0), 0), vline)
}
}
fn find_vline_init_info_forward(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
(start, start_line): (VLine, usize),
vline: VLine,
) -> Option<(usize, RVLine)> {
if start > vline {
return None;
}
let rope_text = text_prov.rope_text();
let mut cur_line = start_line;
let mut cur_vline = start.get();
let layouts = lines.text_layouts.borrow();
while cur_vline < vline.get() {
let font_size = lines.font_size(cur_line);
let line_count = if let Some(text_layout) = layouts.get(font_size, cur_line) {
let line_count = text_layout.line_count();
if cur_vline + line_count > vline.get() {
let line_index = vline.get() - cur_vline;
let col = text_layout
.start_layout_cols(text_prov, cur_line)
.nth(line_index)
.unwrap_or(0);
let col = text_prov.before_phantom_col(cur_line, col);
let offset = rope_text.offset_of_line_col(cur_line, col);
return Some((offset, RVLine::new(cur_line, line_index)));
}
line_count
} else {
1
};
cur_line += 1;
cur_vline += line_count;
}
if cur_vline == vline.get() {
if cur_line > rope_text.last_line() {
return None;
}
Some((rope_text.offset_of_line(cur_line), RVLine::new(cur_line, 0)))
} else {
None
}
}
fn find_vline_init_info_rv_backward(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
(start, start_rvline): (VLine, RVLine),
vline: VLine,
) -> Option<(usize, RVLine)> {
if start < vline {
return None;
}
let shifted_start = VLine(start.get() - start_rvline.line_index);
match shifted_start.cmp(&vline) {
Ordering::Equal => {
let offset = text_prov.rope_text().offset_of_line(start_rvline.line);
Some((offset, RVLine::new(start_rvline.line, 0)))
}
Ordering::Less => {
let line_index = vline.get() - shifted_start.get();
let layouts = lines.text_layouts.borrow();
let font_size = lines.font_size(start_rvline.line);
if let Some(text_layout) = layouts.get(font_size, start_rvline.line) {
vline_init_info_b(
text_prov,
text_layout,
RVLine::new(start_rvline.line, line_index),
)
} else {
let base_offset = text_prov.rope_text().offset_of_line(start_rvline.line);
Some((base_offset, RVLine::new(start_rvline.line, 0)))
}
}
Ordering::Greater => find_vline_init_info_backward(
lines,
text_prov,
(shifted_start, start_rvline.line),
vline,
),
}
}
fn find_vline_init_info_backward(
lines: &Lines,
text_prov: &impl TextLayoutProvider,
(mut start, mut start_line): (VLine, usize),
vline: VLine,
) -> Option<(usize, RVLine)> {
loop {
let (prev_vline, prev_line) = prev_line_start(lines, start, start_line)?;
match prev_vline.cmp(&vline) {
Ordering::Equal => {
let offset = text_prov.rope_text().offset_of_line(prev_line);
return Some((offset, RVLine::new(prev_line, 0)));
}
Ordering::Less => {
let font_size = lines.font_size(prev_line);
let layouts = lines.text_layouts.borrow();
if let Some(text_layout) = layouts.get(font_size, prev_line) {
return vline_init_info_b(
text_prov,
text_layout,
RVLine::new(prev_line, vline.get() - prev_vline.get()),
);
} else {
let base_offset = text_prov.rope_text().offset_of_line(prev_line);
return Some((base_offset, RVLine::new(prev_line, 0)));
}
}
Ordering::Greater => {
start = prev_vline;
start_line = prev_line;
}
}
}
}
fn prev_line_start(lines: &Lines, vline: VLine, line: usize) -> Option<(VLine, usize)> {
if line == 0 {
return None;
}
let layouts = lines.text_layouts.borrow();
let prev_line = line - 1;
let font_size = lines.font_size(line);
if let Some(layout) = layouts.get(font_size, prev_line) {
let line_count = layout.line_count();
let prev_vline = vline.get() - line_count;
Some((VLine(prev_vline), prev_line))
} else {
Some((VLine(vline.get() - 1), prev_line))
}
}
fn vline_init_info_b(
text_prov: &impl TextLayoutProvider,
text_layout: &TextLayoutLine,
rv: RVLine,
) -> Option<(usize, RVLine)> {
let rope_text = text_prov.rope_text();
let col = text_layout
.start_layout_cols(text_prov, rv.line)
.nth(rv.line_index)
.unwrap_or(0);
let col = text_prov.before_phantom_col(rv.line, col);
let offset = rope_text.offset_of_line_col(rv.line, col);
Some((offset, rv))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct VLineInfo<L = VLine> {
pub interval: Interval,
pub line_count: usize,
pub rvline: RVLine,
pub vline: L,
}
impl<L: std::fmt::Debug> VLineInfo<L> {
pub fn new<I: Into<Interval>>(iv: I, rvline: RVLine, line_count: usize, vline: L) -> Self {
Self {
interval: iv.into(),
line_count,
rvline,
vline,
}
}
pub fn to_blank(&self) -> VLineInfo<()> {
VLineInfo::new(self.interval, self.rvline, self.line_count, ())
}
pub fn is_empty(&self) -> bool {
self.interval.is_empty()
}
pub fn is_empty_phantom(&self) -> bool {
self.is_empty() && self.rvline.line_index != 0
}
pub fn is_first(&self) -> bool {
self.rvline.is_first()
}
pub fn is_last(&self, text_prov: &impl TextLayoutProvider) -> bool {
let rope_text = text_prov.rope_text();
let line_end = rope_text.line_end_offset(self.rvline.line, false);
let vline_end = self.line_end_offset(text_prov, false);
line_end == vline_end
}
pub fn first_col(&self, text_prov: &impl TextLayoutProvider) -> usize {
let line_start = self.interval.start;
let start_offset = text_prov.text().offset_of_line(self.rvline.line);
line_start - start_offset
}
pub fn last_col(&self, text_prov: &impl TextLayoutProvider, caret: bool) -> usize {
let vline_end = self.interval.end;
let start_offset = text_prov.text().offset_of_line(self.rvline.line);
if !caret && !self.is_empty() {
let vline_pre_end = text_prov.rope_text().prev_grapheme_offset(vline_end, 1, 0);
vline_pre_end - start_offset
} else {
vline_end - start_offset
}
}
pub fn line_end_offset(&self, text_prov: &impl TextLayoutProvider, caret: bool) -> usize {
let text = text_prov.text();
let rope_text = text_prov.rope_text();
let mut offset = self.interval.end;
let mut line_content: &str = &text.slice_to_cow(self.interval);
if line_content.ends_with("\r\n") {
offset -= 2;
line_content = &line_content[..line_content.len() - 2];
} else if line_content.ends_with('\n') {
offset -= 1;
line_content = &line_content[..line_content.len() - 1];
}
if !caret && !line_content.is_empty() {
offset = rope_text.prev_grapheme_offset(offset, 1, 0);
}
offset
}
pub fn first_non_blank_character(&self, text_prov: &impl TextLayoutProvider) -> usize {
WordCursor::new(&text_prov.text(), self.interval.start).next_non_blank_char()
}
}
struct VisualLines<T: TextLayoutProvider> {
v: VisualLinesRelative<T>,
vline: VLine,
}
impl<T: TextLayoutProvider> VisualLines<T> {
pub fn new(lines: &Lines, text_prov: T, backwards: bool, start: VLine) -> VisualLines<T> {
let Some((_offset, rvline)) = find_vline_init_info(lines, &text_prov, start) else {
return VisualLines::empty(lines, text_prov, backwards);
};
VisualLines {
v: VisualLinesRelative::new(lines, text_prov, backwards, rvline),
vline: start,
}
}
pub fn empty(lines: &Lines, text_prov: T, backwards: bool) -> VisualLines<T> {
VisualLines {
v: VisualLinesRelative::empty(lines, text_prov, backwards),
vline: VLine(0),
}
}
}
impl<T: TextLayoutProvider> Iterator for VisualLines<T> {
type Item = VLineInfo;
fn next(&mut self) -> Option<VLineInfo> {
let was_first_iter = self.v.is_first_iter;
let info = self.v.next()?;
if !was_first_iter {
if self.v.backwards {
debug_assert!(
self.vline.get() != 0,
"Expected VLine to always be nonzero if we were going backwards"
);
self.vline = VLine(self.vline.get().saturating_sub(1));
} else {
self.vline = VLine(self.vline.get() + 1);
}
}
Some(VLineInfo {
interval: info.interval,
line_count: info.line_count,
rvline: info.rvline,
vline: self.vline,
})
}
}
struct VisualLinesRelative<T: TextLayoutProvider> {
font_sizes: Rc<dyn LineFontSizeProvider>,
text_layouts: Rc<RefCell<TextLayoutCache>>,
text_prov: T,
is_done: bool,
rvline: RVLine,
offset: usize,
backwards: bool,
linear: bool,
is_first_iter: bool,
}
impl<T: TextLayoutProvider> VisualLinesRelative<T> {
pub fn new(
lines: &Lines,
text_prov: T,
backwards: bool,
start: RVLine,
) -> VisualLinesRelative<T> {
if start > lines.last_rvline(&text_prov) {
return VisualLinesRelative::empty(lines, text_prov, backwards);
}
let layouts = lines.text_layouts.borrow();
let font_size = lines.font_size(start.line);
let offset = rvline_offset(&layouts, &text_prov, font_size, start);
let linear = lines.is_linear(&text_prov);
VisualLinesRelative {
font_sizes: lines.font_sizes.borrow().clone(),
text_layouts: lines.text_layouts.clone(),
text_prov,
is_done: false,
rvline: start,
offset,
backwards,
linear,
is_first_iter: true,
}
}
pub fn empty(lines: &Lines, text_prov: T, backwards: bool) -> VisualLinesRelative<T> {
VisualLinesRelative {
font_sizes: lines.font_sizes.borrow().clone(),
text_layouts: lines.text_layouts.clone(),
text_prov,
is_done: true,
rvline: RVLine::new(0, 0),
offset: 0,
backwards,
linear: true,
is_first_iter: true,
}
}
}
impl<T: TextLayoutProvider> Iterator for VisualLinesRelative<T> {
type Item = VLineInfo<()>;
fn next(&mut self) -> Option<Self::Item> {
if self.is_done {
return None;
}
let layouts = self.text_layouts.borrow();
if self.is_first_iter {
self.is_first_iter = false;
} else {
let v = shift_rvline(
&layouts,
&self.text_prov,
&*self.font_sizes,
self.rvline,
self.backwards,
self.linear,
);
let Some((new_rel_vline, offset)) = v else {
self.is_done = true;
return None;
};
self.rvline = new_rel_vline;
self.offset = offset;
if self.rvline.line > self.text_prov.rope_text().last_line() {
self.is_done = true;
return None;
}
}
let line = self.rvline.line;
let line_index = self.rvline.line_index;
let vline = self.rvline;
let start = self.offset;
let font_size = self.font_sizes.font_size(line);
let end = end_of_rvline(&layouts, &self.text_prov, font_size, self.rvline);
let line_count = if let Some(text_layout) = layouts.get(font_size, line) {
text_layout.line_count()
} else {
1
};
debug_assert!(start <= end, "line: {line}, line_index: {line_index}, line_count: {line_count}, vline: {vline:?}, start: {start}, end: {end}, backwards: {} text_len: {}", self.backwards, self.text_prov.text().len());
let info = VLineInfo::new(start..end, self.rvline, line_count, ());
Some(info)
}
}
fn end_of_rvline(
layouts: &TextLayoutCache,
text_prov: &impl TextLayoutProvider,
font_size: usize,
RVLine { line, line_index }: RVLine,
) -> usize {
if line > text_prov.rope_text().last_line() {
return text_prov.text().len();
}
if let Some((_, end_col)) = layouts.get_layout_col(text_prov, font_size, line, line_index) {
let end_col = text_prov.before_phantom_col(line, end_col);
text_prov.rope_text().offset_of_line_col(line, end_col)
} else {
let rope_text = text_prov.rope_text();
rope_text.line_end_offset(line, true)
}
}
fn shift_rvline(
layouts: &TextLayoutCache,
text_prov: &impl TextLayoutProvider,
font_sizes: &dyn LineFontSizeProvider,
vline: RVLine,
backwards: bool,
linear: bool,
) -> Option<(RVLine, usize)> {
if linear {
let rope_text = text_prov.rope_text();
debug_assert_eq!(
vline.line_index, 0,
"Line index should be zero if we're linearly working with lines"
);
if backwards {
if vline.line == 0 {
return None;
}
let prev_line = vline.line - 1;
let offset = rope_text.offset_of_line(prev_line);
Some((RVLine::new(prev_line, 0), offset))
} else {
let next_line = vline.line + 1;
if next_line > rope_text.last_line() {
return None;
}
let offset = rope_text.offset_of_line(next_line);
Some((RVLine::new(next_line, 0), offset))
}
} else if backwards {
prev_rvline(layouts, text_prov, font_sizes, vline)
} else {
let font_size = font_sizes.font_size(vline.line);
Some(next_rvline(layouts, text_prov, font_size, vline))
}
}
fn rvline_offset(
layouts: &TextLayoutCache,
text_prov: &impl TextLayoutProvider,
font_size: usize,
RVLine { line, line_index }: RVLine,
) -> usize {
let rope_text = text_prov.rope_text();
if let Some((line_col, _)) = layouts.get_layout_col(text_prov, font_size, line, line_index) {
let line_col = text_prov.before_phantom_col(line, line_col);
rope_text.offset_of_line_col(line, line_col)
} else {
debug_assert_eq!(line_index, 0);
rope_text.offset_of_line(line)
}
}
fn next_rvline(
layouts: &TextLayoutCache,
text_prov: &impl TextLayoutProvider,
font_size: usize,
RVLine { line, line_index }: RVLine,
) -> (RVLine, usize) {
let rope_text = text_prov.rope_text();
if let Some(layout_line) = layouts.get(font_size, line) {
if let Some((line_col, _)) = layout_line.layout_cols(text_prov, line).nth(line_index + 1) {
let line_col = text_prov.before_phantom_col(line, line_col);
let offset = rope_text.offset_of_line_col(line, line_col);
(RVLine::new(line, line_index + 1), offset)
} else {
(RVLine::new(line + 1, 0), rope_text.offset_of_line(line + 1))
}
} else {
debug_assert_eq!(line_index, 0);
(RVLine::new(line + 1, 0), rope_text.offset_of_line(line + 1))
}
}
fn prev_rvline(
layouts: &TextLayoutCache,
text_prov: &impl TextLayoutProvider,
font_sizes: &dyn LineFontSizeProvider,
RVLine { line, line_index }: RVLine,
) -> Option<(RVLine, usize)> {
let rope_text = text_prov.rope_text();
if line_index == 0 {
if line == 0 {
return None;
}
let prev_line = line - 1;
let font_size = font_sizes.font_size(prev_line);
if let Some(layout_line) = layouts.get(font_size, prev_line) {
let (i, line_col) = layout_line
.start_layout_cols(text_prov, prev_line)
.enumerate()
.last()
.unwrap_or((0, 0));
let line_col = text_prov.before_phantom_col(prev_line, line_col);
let offset = rope_text.offset_of_line_col(prev_line, line_col);
Some((RVLine::new(prev_line, i), offset))
} else {
let prev_line_offset = rope_text.offset_of_line(prev_line);
Some((RVLine::new(prev_line, 0), prev_line_offset))
}
} else {
let prev_line_index = line_index - 1;
let font_size = font_sizes.font_size(line);
if let Some(layout_line) = layouts.get(font_size, line) {
if let Some((line_col, _)) = layout_line
.layout_cols(text_prov, line)
.nth(prev_line_index)
{
let line_col = text_prov.before_phantom_col(line, line_col);
let offset = rope_text.offset_of_line_col(line, line_col);
Some((RVLine::new(line, prev_line_index), offset))
} else {
let prev_line_offset = rope_text.offset_of_line(line - 1);
Some((RVLine::new(line - 1, 0), prev_line_offset))
}
} else {
debug_assert!(
false,
"line_index was nonzero but there was no text layout line"
);
let line_offset = rope_text.offset_of_line(line);
Some((RVLine::new(line, 0), line_offset))
}
}
}
pub fn hit_position_aff(this: &TextLayout, idx: usize, before: bool) -> HitPosition {
let mut last_line = 0;
let mut last_end: usize = 0;
let mut offset = 0;
let mut last_glyph: Option<(&LayoutGlyph, usize)> = None;
let mut last_line_width = 0.0;
let mut last_glyph_width = 0.0;
let mut last_position = HitPosition {
line: 0,
point: Point::ZERO,
glyph_ascent: 0.0,
glyph_descent: 0.0,
};
for (line, run) in this.layout_runs().enumerate() {
if run.line_i > last_line {
last_line = run.line_i;
offset += last_end;
}
if let Some((last_glyph, last_offset)) = last_glyph {
if let Some(first_glyph) = run.glyphs.first() {
let end = last_glyph.end + last_offset;
if before && idx == first_glyph.start + offset {
last_position.point.x = if end == idx {
last_line_width as f64
} else {
(last_line_width + last_glyph.w) as f64
};
return last_position;
}
}
}
for glyph in run.glyphs {
if glyph.start + offset > idx {
last_position.point.x += last_glyph_width as f64;
return last_position;
}
last_end = glyph.end;
last_glyph_width = glyph.w;
last_position = HitPosition {
line,
point: Point::new(glyph.x as f64, run.line_y as f64),
glyph_ascent: run.max_ascent as f64,
glyph_descent: run.max_descent as f64,
};
if (glyph.start + offset..glyph.end + offset).contains(&idx) {
return last_position;
}
}
last_glyph = run.glyphs.last().map(|g| (g, offset));
last_line_width = run.line_w;
}
if idx > 0 {
last_position.point.x += last_glyph_width as f64;
return last_position;
}
HitPosition {
line: 0,
point: Point::ZERO,
glyph_ascent: 0.0,
glyph_descent: 0.0,
}
}
#[cfg(test)]
mod tests {
use std::{borrow::Cow, cell::RefCell, collections::HashMap, rc::Rc, sync::Arc};
use floem_editor_core::{
buffer::rope_text::{RopeText, RopeTextRef, RopeTextVal},
cursor::CursorAffinity,
};
use floem_reactive::Scope;
use floem_renderer::text::{Attrs, AttrsList, FamilyOwned, TextLayout, Wrap};
use lapce_xi_rope::Rope;
use smallvec::smallvec;
use crate::views::editor::{
layout::TextLayoutLine,
phantom_text::{PhantomText, PhantomTextKind, PhantomTextLine},
visual_line::{end_of_rvline, find_vline_of_line_backwards, find_vline_of_line_forwards},
};
use super::{
find_vline_init_info_forward, find_vline_init_info_rv_backward, ConfigId, FontSizeCacheId,
LineFontSizeProvider, Lines, RVLine, ResolvedWrap, TextLayoutProvider, VLine,
};
const FONT_SIZE: usize = 12;
struct TestTextLayoutProvider<'a> {
text: &'a Rope,
phantom: HashMap<usize, PhantomTextLine>,
font_family: Vec<FamilyOwned>,
#[allow(dead_code)]
wrap: Wrap,
}
impl<'a> TestTextLayoutProvider<'a> {
fn new(text: &'a Rope, ph: HashMap<usize, PhantomTextLine>, wrap: Wrap) -> Self {
Self {
text,
phantom: ph,
#[cfg(not(target_os = "windows"))]
font_family: vec![FamilyOwned::SansSerif],
#[cfg(target_os = "windows")]
font_family: vec![FamilyOwned::Name("Arial".to_string())],
wrap,
}
}
}
impl<'a> TextLayoutProvider for TestTextLayoutProvider<'a> {
fn text(&self) -> Rope {
self.text.clone()
}
fn new_text_layout(
&self,
line: usize,
font_size: usize,
wrap: ResolvedWrap,
) -> Arc<TextLayoutLine> {
let rope_text = RopeTextRef::new(self.text);
let line_content_original = rope_text.line_content(line);
let (line_content, _line_content_original) =
if let Some(s) = line_content_original.strip_suffix("\r\n") {
(
format!("{s} "),
&line_content_original[..line_content_original.len() - 2],
)
} else if let Some(s) = line_content_original.strip_suffix('\n') {
(
format!("{s} ",),
&line_content_original[..line_content_original.len() - 1],
)
} else {
(
line_content_original.to_string(),
&line_content_original[..],
)
};
let phantom_text = self.phantom.get(&line).cloned().unwrap_or_default();
let line_content = phantom_text.combine_with_text(&line_content);
let attrs = Attrs::new()
.family(&self.font_family)
.font_size(font_size as f32);
let mut attrs_list = AttrsList::new(attrs);
for (offset, size, col, phantom) in phantom_text.offset_size_iter() {
let start = col + offset;
let end = start + size;
let mut attrs = attrs;
if let Some(fg) = phantom.fg {
attrs = attrs.color(fg);
}
if let Some(phantom_font_size) = phantom.font_size {
attrs = attrs.font_size(phantom_font_size.min(font_size) as f32);
}
attrs_list.add_span(start..end, attrs);
}
let mut text_layout = TextLayout::new();
text_layout.set_wrap(Wrap::Word);
match wrap {
ResolvedWrap::None => {}
ResolvedWrap::Column(_col) => todo!(),
ResolvedWrap::Width(px) => {
text_layout.set_size(px, f32::MAX);
}
}
text_layout.set_text(&line_content, attrs_list);
Arc::new(TextLayoutLine {
extra_style: Vec::new(),
text: text_layout,
whitespaces: None,
indent: 0.0,
phantom_text: PhantomTextLine::default(),
})
}
fn before_phantom_col(&self, line: usize, col: usize) -> usize {
self.phantom
.get(&line)
.map(|x| x.before_col(col))
.unwrap_or(col)
}
fn has_multiline_phantom(&self) -> bool {
true
}
}
struct TestFontSize {
font_size: usize,
}
impl LineFontSizeProvider for TestFontSize {
fn font_size(&self, _line: usize) -> usize {
self.font_size
}
fn cache_id(&self) -> FontSizeCacheId {
0
}
}
fn make_lines(text: &Rope, width: f32, init: bool) -> (TestTextLayoutProvider<'_>, Lines) {
make_lines_ph(text, width, init, HashMap::new())
}
fn make_lines_ph(
text: &Rope,
width: f32,
init: bool,
ph: HashMap<usize, PhantomTextLine>,
) -> (TestTextLayoutProvider<'_>, Lines) {
let wrap = Wrap::Word;
let r_wrap = ResolvedWrap::Width(width);
let font_sizes = TestFontSize {
font_size: FONT_SIZE,
};
let text = TestTextLayoutProvider::new(text, ph, wrap);
let cx = Scope::new();
let lines = Lines::new(cx, RefCell::new(Rc::new(font_sizes)));
lines.set_wrap(r_wrap);
if init {
let config_id = 0;
let floem_style_id = 0;
lines.init_all(0, ConfigId::new(config_id, floem_style_id), &text, true);
}
(text, lines)
}
fn render_breaks<'a>(text: &'a Rope, lines: &mut Lines, font_size: usize) -> Vec<Cow<'a, str>> {
let rope_text = RopeTextRef::new(text);
let mut result = Vec::new();
let layouts = lines.text_layouts.borrow();
for line in 0..rope_text.num_lines() {
if let Some(text_layout) = layouts.get(font_size, line) {
for line in text_layout.text.lines() {
let layouts = line.layout_opt().as_deref().unwrap();
for layout in layouts {
if layout.glyphs.is_empty() {
continue;
}
let start_idx = layout.glyphs[0].start;
let end_idx = layout.glyphs.last().unwrap().end;
let line_content = line
.text()
.get(start_idx..=end_idx)
.unwrap_or(&line.text()[start_idx..end_idx]);
result.push(Cow::Owned(line_content.to_string()));
}
}
} else {
let line_content = rope_text.line_content(line);
let line_content = match line_content {
Cow::Borrowed(x) => {
if let Some(x) = x.strip_suffix('\n') {
Cow::Owned(x.to_string())
} else {
Cow::Owned(x.to_string())
}
}
Cow::Owned(x) => {
if let Some(x) = x.strip_suffix('\n') {
Cow::Owned(x.to_string())
} else {
Cow::Owned(x)
}
}
};
result.push(line_content);
}
}
result
}
fn mph(kind: PhantomTextKind, col: usize, text: &str) -> PhantomText {
PhantomText {
kind,
col,
affinity: None,
text: text.to_string(),
font_size: None,
fg: None,
bg: None,
under_line: None,
}
}
fn ffvline_info(
lines: &Lines,
text_prov: impl TextLayoutProvider,
vline: VLine,
) -> Option<(usize, RVLine)> {
find_vline_init_info_forward(lines, &text_prov, (VLine(0), 0), vline)
}
fn fbvline_info(
lines: &Lines,
text_prov: impl TextLayoutProvider,
vline: VLine,
) -> Option<(usize, RVLine)> {
let last_vline = lines.last_vline(&text_prov);
let last_rvline = lines.last_rvline(&text_prov);
find_vline_init_info_rv_backward(lines, &text_prov, (last_vline, last_rvline), vline)
}
#[test]
fn find_vline_init_info_empty() {
let text = Rope::from("");
let (text_prov, lines) = make_lines(&text, 50.0, false);
assert_eq!(
ffvline_info(&lines, &text_prov, VLine(0)),
Some((0, RVLine::new(0, 0)))
);
assert_eq!(
fbvline_info(&lines, &text_prov, VLine(0)),
Some((0, RVLine::new(0, 0)))
);
assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None);
let text = Rope::from("");
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 0, "hello world abc")],
},
);
let (text_prov, lines) = make_lines_ph(&text, 20.0, false, ph);
assert_eq!(
ffvline_info(&lines, &text_prov, VLine(0)),
Some((0, RVLine::new(0, 0)))
);
assert_eq!(
fbvline_info(&lines, &text_prov, VLine(0)),
Some((0, RVLine::new(0, 0)))
);
assert_eq!(ffvline_info(&lines, &text_prov, VLine(1)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(1)), None);
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
assert_eq!(
ffvline_info(&lines, &text_prov, VLine(0)),
Some((0, RVLine::new(0, 0)))
);
assert_eq!(
fbvline_info(&lines, &text_prov, VLine(0)),
Some((0, RVLine::new(0, 0)))
);
assert_eq!(
ffvline_info(&lines, &text_prov, VLine(1)),
Some((0, RVLine::new(0, 1)))
);
assert_eq!(
fbvline_info(&lines, &text_prov, VLine(1)),
Some((0, RVLine::new(0, 1)))
);
assert_eq!(
ffvline_info(&lines, &text_prov, VLine(2)),
Some((0, RVLine::new(0, 2)))
);
assert_eq!(
fbvline_info(&lines, &text_prov, VLine(2)),
Some((0, RVLine::new(0, 2)))
);
assert_eq!(ffvline_info(&lines, &text_prov, VLine(3)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(3)), None);
}
#[test]
fn find_vline_init_info_unwrapping() {
let text = Rope::from("hello\nworld toast and jam\nthe end\nhi");
let rope_text = RopeTextRef::new(&text);
let (text_prov, mut lines) = make_lines(&text, 500.0, false);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["hello", "world toast and jam", "the end", "hi"]
);
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["hello ", "world toast and jam ", "the end ", "hi"]
);
}
#[test]
fn find_vline_init_info_phantom_unwrapping() {
let text = Rope::from("hello\nworld toast and jam\nthe end\nhi");
let rope_text = RopeTextRef::new(&text);
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 0, "greet world")],
},
);
let (text_prov, lines) = make_lines_ph(&text, 500.0, false, ph);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 0, "greet\nworld"),],
},
);
let (text_prov, mut lines) = make_lines_ph(&text, 500.0, false, ph);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
[
"greet",
"worldhello ",
"world toast and jam ",
"the end ",
"hi"
]
);
let info = ffvline_info(&lines, &text_prov, VLine(0));
assert_eq!(info, Some((0, RVLine::new(0, 0))));
let info = fbvline_info(&lines, &text_prov, VLine(0));
assert_eq!(info, Some((0, RVLine::new(0, 0))));
let info = ffvline_info(&lines, &text_prov, VLine(1));
assert_eq!(info, Some((0, RVLine::new(0, 1))));
let info = fbvline_info(&lines, &text_prov, VLine(1));
assert_eq!(info, Some((0, RVLine::new(0, 1))));
for line in 2..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line - 1);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(
info,
(line_offset, RVLine::new(line - 1, 0)),
"vline {}",
line
);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(
info,
(line_offset, RVLine::new(line - 1, 0)),
"vline {}",
line
);
}
let line_offset = rope_text.offset_of_line(rope_text.last_line());
let info = ffvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1));
assert_eq!(
info,
Some((line_offset, RVLine::new(rope_text.last_line(), 0))),
"line {}",
rope_text.last_line() + 1,
);
let info = fbvline_info(&lines, &text_prov, VLine(rope_text.last_line() + 1));
assert_eq!(
info,
Some((line_offset, RVLine::new(rope_text.last_line(), 0))),
"line {}",
rope_text.last_line() + 1,
);
let mut ph = HashMap::new();
ph.insert(
2, PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 3, "greet\nworld"),],
},
);
let (text_prov, mut lines) = make_lines_ph(&text, 500.0, false, ph);
for line in 0..rope_text.num_lines() {
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
let line_offset = rope_text.offset_of_line(line);
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
[
"hello ",
"world toast and jam ",
"thegreet",
"world end ",
"hi"
]
);
for line in 0..3 {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "vline {}", line);
}
let info = ffvline_info(&lines, &text_prov, VLine(3));
assert_eq!(info, Some((29, RVLine::new(2, 1))));
let info = fbvline_info(&lines, &text_prov, VLine(3));
assert_eq!(info, Some((29, RVLine::new(2, 1))));
let info = ffvline_info(&lines, &text_prov, VLine(4));
assert_eq!(info, Some((34, RVLine::new(3, 0))));
let info = fbvline_info(&lines, &text_prov, VLine(4));
assert_eq!(info, Some((34, RVLine::new(3, 0))));
}
#[test]
fn find_vline_init_info_basic_wrapping() {
let text = Rope::from("hello\nworld toast and jam\nthe end\nhi");
let rope_text = RopeTextRef::new(&text);
let (text_prov, mut lines) = make_lines(&text, 30.0, false);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(info, (line_offset, RVLine::new(line, 0)), "line {}", line);
}
assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["hello", "world toast and jam", "the end", "hi"]
);
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
{
let layouts = lines.text_layouts.borrow();
assert!(layouts.get(FONT_SIZE, 0).is_some());
assert!(layouts.get(FONT_SIZE, 1).is_some());
assert!(layouts.get(FONT_SIZE, 2).is_some());
assert!(layouts.get(FONT_SIZE, 3).is_some());
assert!(layouts.get(FONT_SIZE, 4).is_none());
}
let line_data = [
(0, 0, 0),
(6, 1, 0),
(12, 1, 1),
(18, 1, 2),
(22, 1, 3),
(26, 2, 0),
(30, 2, 1),
(34, 3, 0),
];
assert_eq!(lines.last_vline(&text_prov), VLine(7));
assert_eq!(lines.last_rvline(&text_prov), RVLine::new(3, 0));
#[allow(clippy::needless_range_loop)]
for line in 0..8 {
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(
(info.0, info.1.line, info.1.line_index),
line_data[line],
"vline {}",
line
);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(
(info.0, info.1.line, info.1.line_index),
line_data[line],
"vline {}",
line
);
}
assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None,);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None,);
assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["hello ", "world ", "toast ", "and ", "jam ", "the ", "end ", "hi"]
);
let vline_line_data = [0, 1, 5, 7];
let rope = text_prov.rope_text();
let last_start_vline =
VLine(lines.last_vline(&text_prov).get() - lines.last_rvline(&text_prov).line_index);
#[allow(clippy::needless_range_loop)]
for line in 0..4 {
let vline = VLine(vline_line_data[line]);
assert_eq!(
find_vline_of_line_forwards(&lines, Default::default(), line),
Some(vline)
);
assert_eq!(
find_vline_of_line_backwards(&lines, (last_start_vline, rope.last_line()), line),
Some(vline),
"line: {line}"
);
}
let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into();
let (text_prov, mut lines) = make_lines(&text, 2., true);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["aaaa ", "bb ", "bb ", "cc ", "cc ", "dddd ", "eeee ", "ff ", "ff ", "gggg"]
);
let line_data = [
(0, 0, 0),
(5, 1, 0),
(8, 1, 1),
(11, 1, 2),
(14, 2, 0),
(17, 2, 1),
(22, 2, 2),
(27, 2, 3),
(30, 3, 0),
(33, 3, 1),
];
#[allow(clippy::needless_range_loop)]
for vline in 0..10 {
let info = ffvline_info(&lines, &text_prov, VLine(vline)).unwrap();
assert_eq!(
(info.0, info.1.line, info.1.line_index),
line_data[vline],
"vline {}",
vline
);
let info = fbvline_info(&lines, &text_prov, VLine(vline)).unwrap();
assert_eq!(
(info.0, info.1.line, info.1.line_index),
line_data[vline],
"vline {}",
vline
);
}
let vline_line_data = [0, 1, 4, 8];
let rope = text_prov.rope_text();
let last_start_vline =
VLine(lines.last_vline(&text_prov).get() - lines.last_rvline(&text_prov).line_index);
#[allow(clippy::needless_range_loop)]
for line in 0..4 {
let vline = VLine(vline_line_data[line]);
assert_eq!(
find_vline_of_line_forwards(&lines, Default::default(), line),
Some(vline)
);
assert_eq!(
find_vline_of_line_backwards(&lines, (last_start_vline, rope.last_line()), line),
Some(vline),
"line: {line}"
);
}
}
#[test]
fn find_vline_init_info_basic_wrapping_phantom() {
let text = Rope::from("hello\nworld toast and jam\nthe end\nhi");
let rope_text = RopeTextRef::new(&text);
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 0, "greet world")],
},
);
let (text_prov, mut lines) = make_lines_ph(&text, 30.0, false, ph);
for line in 0..rope_text.num_lines() {
let line_offset = rope_text.offset_of_line(line);
let info = ffvline_info(&lines, &text_prov, VLine(line));
assert_eq!(
info,
Some((line_offset, RVLine::new(line, 0))),
"line {}",
line
);
let info = fbvline_info(&lines, &text_prov, VLine(line));
assert_eq!(
info,
Some((line_offset, RVLine::new(line, 0))),
"line {}",
line
);
}
assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["hello", "world toast and jam", "the end", "hi"]
);
lines.init_all(0, ConfigId::new(0, 0), &text_prov, true);
{
let layouts = lines.text_layouts.borrow();
assert!(layouts.get(FONT_SIZE, 0).is_some());
assert!(layouts.get(FONT_SIZE, 1).is_some());
assert!(layouts.get(FONT_SIZE, 2).is_some());
assert!(layouts.get(FONT_SIZE, 3).is_some());
assert!(layouts.get(FONT_SIZE, 4).is_none());
}
let line_data = [
(0, 0, 0),
(0, 0, 1),
(6, 1, 0),
(12, 1, 1),
(18, 1, 2),
(22, 1, 3),
(26, 2, 0),
(30, 2, 1),
(34, 3, 0),
];
#[allow(clippy::needless_range_loop)]
for line in 0..9 {
let info = ffvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(
(info.0, info.1.line, info.1.line_index),
line_data[line],
"vline {}",
line
);
let info = fbvline_info(&lines, &text_prov, VLine(line)).unwrap();
assert_eq!(
(info.0, info.1.line, info.1.line_index),
line_data[line],
"vline {}",
line
);
}
assert_eq!(ffvline_info(&lines, &text_prov, VLine(9)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(9)), None);
assert_eq!(ffvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(fbvline_info(&lines, &text_prov, VLine(20)), None);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
[
"greet ",
"worldhello ",
"world ",
"toast ",
"and ",
"jam ",
"the ",
"end ",
"hi"
]
);
}
#[test]
fn num_vlines() {
let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into();
let (text_prov, lines) = make_lines(&text, 2., true);
assert_eq!(lines.num_vlines(&text_prov), 10);
let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into();
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 0, "greet\nworld")],
},
);
let (text_prov, lines) = make_lines_ph(&text, 2., true, ph);
assert_eq!(lines.num_vlines(&text_prov), 11);
}
#[test]
fn offset_to_line() {
let text = "a b c d ".into();
let (text_prov, lines) = make_lines(&text, 1., true);
assert_eq!(lines.num_vlines(&text_prov), 4);
let vlines = [0, 0, 1, 1, 2, 2, 3, 3];
for (i, v) in vlines.iter().enumerate() {
assert_eq!(
lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward),
VLine(*v),
"offset: {i}"
);
}
assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8);
for offset in 0..text.len() {
let line = lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward);
let line_offset = lines.offset_of_vline(&text_prov, line);
assert!(
line_offset <= offset,
"{} <= {} L{:?} O{}",
line_offset,
offset,
line,
offset
);
}
let text = "blah\n\n\nhi\na b c d e".into();
let (text_prov, lines) = make_lines(&text, 12.0 * 3.0, true);
let vlines = [0, 0, 0, 0, 0];
for (i, v) in vlines.iter().enumerate() {
assert_eq!(
lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward),
VLine(*v),
"offset: {i}"
);
}
assert_eq!(
lines
.vline_of_offset(&text_prov, 4, CursorAffinity::Backward)
.get(),
0
);
assert_eq!(
lines
.vline_of_offset(&text_prov, 5, CursorAffinity::Forward)
.get(),
1
);
assert_eq!(
lines
.vline_of_offset(&text_prov, 5, CursorAffinity::Backward)
.get(),
1
);
assert_eq!(
lines
.vline_of_offset(&text_prov, 16, CursorAffinity::Forward)
.get(),
5
);
assert_eq!(
lines
.vline_of_offset(&text_prov, 16, CursorAffinity::Backward)
.get(),
4
);
assert_eq!(
lines.vline_of_offset(&text_prov, 20, CursorAffinity::Forward),
lines.last_vline(&text_prov)
);
let text = "a\nb\nc\n".into();
let (text_prov, lines) = make_lines(&text, 1., true);
assert_eq!(lines.num_vlines(&text_prov), 4);
let vlines = [0, 0, 1, 1, 2, 2, 3, 3];
for (i, v) in vlines.iter().enumerate() {
assert_eq!(
lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward),
VLine(*v),
"offset: {i}"
);
assert_eq!(
lines.vline_of_offset(&text_prov, i, CursorAffinity::Backward),
VLine(*v),
"offset: {i}"
);
}
let text =
Rope::from("asdf\nposition: Some(EditorPosition::Offset(self.offset))\nasdf\nasdf");
let (text_prov, mut lines) = make_lines(&text, 1., true);
println!("Breaks: {:?}", render_breaks(&text, &mut lines, FONT_SIZE));
let rvline = lines.rvline_of_offset(&text_prov, 3, CursorAffinity::Backward);
assert_eq!(rvline, RVLine::new(0, 0));
let rvline_info = lines
.iter_rvlines(&text_prov, false, rvline)
.next()
.unwrap();
assert_eq!(rvline_info.rvline, rvline);
let offset = lines.offset_of_rvline(&text_prov, rvline);
assert_eq!(offset, 0);
assert_eq!(
lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward),
VLine(0)
);
assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(0));
let rvline = lines.rvline_of_offset(&text_prov, 7, CursorAffinity::Backward);
assert_eq!(rvline, RVLine::new(1, 0));
let rvline_info = lines
.iter_rvlines(&text_prov, false, rvline)
.next()
.unwrap();
assert_eq!(rvline_info.rvline, rvline);
let offset = lines.offset_of_rvline(&text_prov, rvline);
assert_eq!(offset, 5);
assert_eq!(
lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward),
VLine(1)
);
assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(1));
let rvline = lines.rvline_of_offset(&text_prov, 17, CursorAffinity::Backward);
assert_eq!(rvline, RVLine::new(1, 1));
let rvline_info = lines
.iter_rvlines(&text_prov, false, rvline)
.next()
.unwrap();
assert_eq!(rvline_info.rvline, rvline);
let offset = lines.offset_of_rvline(&text_prov, rvline);
assert_eq!(offset, 15);
assert_eq!(
lines.vline_of_offset(&text_prov, offset, CursorAffinity::Backward),
VLine(1)
);
assert_eq!(
lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward),
VLine(2)
);
assert_eq!(lines.vline_of_rvline(&text_prov, rvline), VLine(2));
}
#[test]
fn offset_to_line_phantom() {
let text = "a b c d ".into();
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 1, "hi")],
},
);
let (text_prov, mut lines) = make_lines_ph(&text, 1., true, ph);
assert_eq!(lines.num_vlines(&text_prov), 4);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["ahi ", "b ", "c ", "d "]
);
let vlines = [0, 0, 1, 1, 2, 2, 3, 3];
for (i, v) in vlines.iter().enumerate() {
assert_eq!(
lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward),
VLine(*v),
"offset: {i}"
);
}
assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8);
for offset in 0..text.len() {
let line = lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward);
let line_offset = lines.offset_of_vline(&text_prov, line);
assert!(
line_offset <= offset,
"{} <= {} L{:?} O{}",
line_offset,
offset,
line,
offset
);
}
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 2, "hi")],
},
);
let (text_prov, mut lines) = make_lines_ph(&text, 1., true, ph);
assert_eq!(lines.num_vlines(&text_prov), 4);
assert_eq!(
render_breaks(&text, &mut lines, FONT_SIZE),
["a ", "hib ", "c ", "d "]
);
for (i, v) in vlines.iter().enumerate() {
assert_eq!(
lines.vline_of_offset(&text_prov, i, CursorAffinity::Forward),
VLine(*v),
"offset: {i}"
);
}
assert_eq!(
lines.vline_of_offset(&text_prov, 2, CursorAffinity::Backward),
VLine(0)
);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(0)), 0);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(1)), 2);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(2)), 4);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(3)), 6);
assert_eq!(lines.offset_of_vline(&text_prov, VLine(10)), 8);
for offset in 0..text.len() {
let line = lines.vline_of_offset(&text_prov, offset, CursorAffinity::Forward);
let line_offset = lines.offset_of_vline(&text_prov, line);
assert!(
line_offset <= offset,
"{} <= {} L{:?} O{}",
line_offset,
offset,
line,
offset
);
}
}
#[test]
fn iter_lines() {
let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into();
let (text_prov, lines) = make_lines(&text, 2., true);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(0))
.take(2)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec!["aaaa", "bb "]);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(1))
.take(2)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec!["bb ", "bb "]);
let v = lines.get_init_text_layout(0, ConfigId::new(0, 0), &text_prov, 2, true);
let v = v.layout_cols(&text_prov, 2).collect::<Vec<_>>();
assert_eq!(v, [(0, 3), (3, 8), (8, 13), (13, 15)]);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(3))
.take(3)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec!["cc", "cc ", "dddd "]);
let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect();
r.reverse();
let r1: Vec<_> = lines
.iter_vlines(&text_prov, true, lines.last_vline(&text_prov))
.collect();
assert_eq!(r, r1);
let rel1: Vec<_> = lines
.iter_rvlines(&text_prov, false, RVLine::new(0, 0))
.map(|i| i.rvline)
.collect();
r.reverse(); assert!(r.iter().map(|i| i.rvline).eq(rel1));
let text: Rope = "".into();
let (text_prov, lines) = make_lines(&text, 2., true);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(0))
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec![""]);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(1))
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, Vec::<&str>::new());
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(2))
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, Vec::<&str>::new());
let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect();
r.reverse();
let r1: Vec<_> = lines
.iter_vlines(&text_prov, true, lines.last_vline(&text_prov))
.collect();
assert_eq!(r, r1);
let rel1: Vec<_> = lines
.iter_rvlines(&text_prov, false, RVLine::new(0, 0))
.map(|i| i.rvline)
.collect();
r.reverse(); assert!(r.iter().map(|i| i.rvline).eq(rel1));
let text: Rope = "".into();
let (text_prov, lines) = make_lines(&text, 2., false);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(0))
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec![""]);
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(1))
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, Vec::<&str>::new());
let r: Vec<_> = lines
.iter_vlines(&text_prov, false, VLine(2))
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, Vec::<&str>::new());
let mut r: Vec<_> = lines.iter_vlines(&text_prov, false, VLine(0)).collect();
r.reverse();
let r1: Vec<_> = lines
.iter_vlines(&text_prov, true, lines.last_vline(&text_prov))
.collect();
assert_eq!(r, r1);
let rel1: Vec<_> = lines
.iter_rvlines(&text_prov, false, RVLine::new(0, 0))
.map(|i| i.rvline)
.collect();
r.reverse(); assert!(r.iter().map(|i| i.rvline).eq(rel1));
}
#[test]
fn init_iter_vlines() {
let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into();
let (text_prov, lines) = make_lines(&text, 2., false);
let r: Vec<_> = lines
.iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(0), true)
.take(2)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec!["aaaa", "bb "]);
let r: Vec<_> = lines
.iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(1), true)
.take(2)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec!["bb ", "bb "]);
let r: Vec<_> = lines
.iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(3), true)
.take(3)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec!["cc", "cc ", "dddd "]);
let text: Rope = "".into();
let (text_prov, lines) = make_lines(&text, 2., false);
let r: Vec<_> = lines
.iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(0), true)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, vec![""]);
let r: Vec<_> = lines
.iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(1), true)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, Vec::<&str>::new());
let r: Vec<_> = lines
.iter_vlines_init(&text_prov, 0, ConfigId::new(0, 0), VLine(2), true)
.map(|l| text.slice_to_cow(l.interval))
.collect();
assert_eq!(r, Vec::<&str>::new());
}
#[test]
fn line_numbers() {
let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into();
let (text_prov, lines) = make_lines(&text, 12.0 * 2.0, true);
let get_nums = |start_vline: usize| {
lines
.iter_vlines(&text_prov, false, VLine(start_vline))
.map(|l| {
(
l.rvline.line,
l.vline.get(),
l.is_first(),
text.slice_to_cow(l.interval),
)
})
.collect::<Vec<_>>()
};
let x = vec![
(0, 0, true, "aaaa".into()),
(1, 1, true, "bb ".into()),
(1, 2, false, "bb ".into()),
(1, 3, false, "cc".into()),
(2, 4, true, "cc ".into()),
(2, 5, false, "dddd ".into()),
(2, 6, false, "eeee ".into()),
(2, 7, false, "ff".into()),
(3, 8, true, "ff ".into()),
(3, 9, false, "gggg".into()),
];
for i in 0..x.len() {
let nums = get_nums(i);
println!("i: {i}, #nums: {}, #&x[i..]: {}", nums.len(), x[i..].len());
assert_eq!(nums, &x[i..], "failed at #{i}");
}
}
#[test]
fn last_col() {
let text: Rope = Rope::from("conf = Config::default();");
let (text_prov, lines) = make_lines(&text, 24.0 * 2.0, true);
let mut iter = lines.iter_rvlines(&text_prov, false, RVLine::default());
let v = iter.next().unwrap();
assert_eq!(v.last_col(&text_prov, false), 6);
assert_eq!(v.last_col(&text_prov, true), 7);
let v = iter.next().unwrap();
assert_eq!(v.last_col(&text_prov, false), 24);
assert_eq!(v.last_col(&text_prov, true), 25);
let text = Rope::from("blah\nthing");
let (text_prov, lines) = make_lines(&text, 1000., false);
let mut iter = lines.iter_rvlines(&text_prov, false, RVLine::default());
let rtext = RopeTextVal::new(text.clone());
let v = iter.next().unwrap();
assert_eq!(v.last_col(&text_prov, false), 3);
assert_eq!(v.last_col(&text_prov, true), 4);
assert_eq!(rtext.offset_of_line_col(0, 3), 3);
assert_eq!(rtext.offset_of_line_col(0, 4), 4);
let v = iter.next().unwrap();
assert_eq!(v.last_col(&text_prov, false), 4);
assert_eq!(v.last_col(&text_prov, true), 5);
let text = Rope::from("blah\r\nthing");
let (text_prov, lines) = make_lines(&text, 1000., false);
let mut iter = lines.iter_rvlines(&text_prov, false, RVLine::default());
let rtext = RopeTextVal::new(text.clone());
let v = iter.next().unwrap();
assert_eq!(v.last_col(&text_prov, false), 3);
assert_eq!(v.last_col(&text_prov, true), 4);
assert_eq!(rtext.offset_of_line_col(0, 3), 3);
assert_eq!(rtext.offset_of_line_col(0, 4), 4);
let v = iter.next().unwrap();
assert_eq!(v.last_col(&text_prov, false), 4);
assert_eq!(v.last_col(&text_prov, true), 5);
assert_eq!(rtext.offset_of_line_col(0, 4), 4);
assert_eq!(rtext.offset_of_line_col(0, 5), 4);
}
#[test]
fn layout_cols() {
let text = Rope::from("aaaa\nbb bb cc\ndd");
let mut layout = TextLayout::new();
layout.set_text("aaaa", AttrsList::new(Attrs::new()));
let layout = TextLayoutLine {
extra_style: Vec::new(),
text: layout,
whitespaces: None,
indent: 0.,
phantom_text: PhantomTextLine::default(),
};
let (text_prov, _) = make_lines(&text, 10000., false);
assert_eq!(
layout.layout_cols(&text_prov, 0).collect::<Vec<_>>(),
vec![(0, 4)]
);
let (text_prov, _) = make_lines(&text, 10000., true);
assert_eq!(
layout.layout_cols(&text_prov, 0).collect::<Vec<_>>(),
vec![(0, 4)]
);
let text = Rope::from("aaaa\r\nbb bb cc\r\ndd");
let mut layout = TextLayout::new();
layout.set_text("aaaa", AttrsList::new(Attrs::new()));
let layout = TextLayoutLine {
extra_style: Vec::new(),
text: layout,
whitespaces: None,
indent: 0.,
phantom_text: PhantomTextLine::default(),
};
let (text_prov, _) = make_lines(&text, 10000., false);
assert_eq!(
layout.layout_cols(&text_prov, 0).collect::<Vec<_>>(),
vec![(0, 4)]
);
let (text_prov, _) = make_lines(&text, 10000., true);
assert_eq!(
layout.layout_cols(&text_prov, 0).collect::<Vec<_>>(),
vec![(0, 4)]
);
}
#[test]
fn test_end_of_rvline() {
fn eor(lines: &Lines, text_prov: &impl TextLayoutProvider, rvline: RVLine) -> usize {
let layouts = lines.text_layouts.borrow();
end_of_rvline(&layouts, text_prov, 12, rvline)
}
fn check_equiv(text: &Rope, expected: usize, from: &str) {
let (text_prov, lines) = make_lines(text, 10000., false);
let end1 = eor(&lines, &text_prov, RVLine::new(0, 0));
let (text_prov, lines) = make_lines(text, 10000., true);
assert_eq!(
eor(&lines, &text_prov, RVLine::new(0, 0)),
end1,
"non-init end_of_rvline not equivalent to init ({from})"
);
assert_eq!(end1, expected, "end_of_rvline not equivalent ({from})");
}
let text = Rope::from("");
check_equiv(&text, 0, "empty");
let text = Rope::from("aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg");
check_equiv(&text, 4, "simple multiline (LF)");
let text = Rope::from("aaaa\r\nbb bb cc\r\ncc dddd eeee ff\r\nff gggg");
check_equiv(&text, 4, "simple multiline (CRLF)");
let text = Rope::from("a b c d ");
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![mph(PhantomTextKind::Completion, 1, "hi")],
},
);
let (text_prov, lines) = make_lines_ph(&text, 1., true, ph);
assert_eq!(eor(&lines, &text_prov, RVLine::new(0, 0)), 2);
assert_eq!(eor(&lines, &text_prov, RVLine::new(0, 1)), 4);
let text = Rope::from(" let j = test_test\nlet blah = 5;");
let mut ph = HashMap::new();
ph.insert(
0,
PhantomTextLine {
text: smallvec![
mph(
PhantomTextKind::Diagnostic,
26,
" Syntax Error: `let` expressions are not supported here"
),
mph(
PhantomTextKind::Diagnostic,
26,
" Syntax Error: expected SEMICOLON"
),
],
},
);
let (text_prov, lines) = make_lines_ph(&text, 250., true, ph);
assert_eq!(eor(&lines, &text_prov, RVLine::new(0, 0)), 25);
assert_eq!(eor(&lines, &text_prov, RVLine::new(0, 1)), 25);
assert_eq!(eor(&lines, &text_prov, RVLine::new(0, 2)), 25);
assert_eq!(eor(&lines, &text_prov, RVLine::new(0, 3)), 25);
assert_eq!(eor(&lines, &text_prov, RVLine::new(1, 0)), 39);
}
#[test]
fn equivalence() {
fn check_equiv(text: &Rope, from: &str) {
let (text_prov, lines) = make_lines(text, 10000., false);
let iter = lines.iter_rvlines(&text_prov, false, RVLine::default());
let (text_prov, lines) = make_lines(text, 01000., true);
let iter2 = lines.iter_rvlines(&text_prov, false, RVLine::default());
for (i, v) in iter.zip(iter2) {
assert_eq!(
i, v,
"Line {} is not equivalent when initting ({from})",
i.rvline.line
);
}
}
check_equiv(&Rope::from(""), "empty");
check_equiv(&Rope::from("a"), "a");
check_equiv(
&Rope::from("aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg"),
"simple multiline (LF)",
);
check_equiv(
&Rope::from("aaaa\r\nbb bb cc\r\ncc dddd eeee ff\r\nff gggg"),
"simple multiline (CRLF)",
);
}
}