Permalink
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up| // Copyright 2016 The xi-editor Authors. | |
| // | |
| // Licensed under the Apache License, Version 2.0 (the "License"); | |
| // you may not use this file except in compliance with the License. | |
| // You may obtain a copy of the License at | |
| // | |
| // http://www.apache.org/licenses/LICENSE-2.0 | |
| // | |
| // Unless required by applicable law or agreed to in writing, software | |
| // distributed under the License is distributed on an "AS IS" BASIS, | |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| // See the License for the specific language governing permissions and | |
| // limitations under the License. | |
| //! Compute line wrapping breaks for text. | |
| use std::cmp::Ordering; | |
| use std::ops::Range; | |
| use xi_rope::breaks::{BreakBuilder, Breaks, BreaksBaseMetric, BreaksInfo, BreaksMetric}; | |
| use xi_rope::rope::BaseMetric; | |
| use xi_rope::spans::Spans; | |
| use xi_rope::{Cursor, Interval, LinesMetric, Rope, RopeDelta, RopeInfo}; | |
| use xi_trace::trace_block; | |
| use xi_unicode::LineBreakLeafIter; | |
| use crate::client::Client; | |
| use crate::styles::{Style, N_RESERVED_STYLES}; | |
| use crate::width_cache::{CodepointMono, Token, WidthCache, WidthMeasure}; | |
| /// The visual width of the buffer for the purpose of word wrapping. | |
| #[derive(Clone, Copy, Debug, PartialEq)] | |
| pub(crate) enum WrapWidth { | |
| /// No wrapping in effect. | |
| None, | |
| /// Width in bytes (utf-8 code units). | |
| /// | |
| /// Only works well for ASCII, will probably not be maintained long-term. | |
| Bytes(usize), | |
| /// Width in px units, requiring measurement by the front-end. | |
| Width(f64), | |
| } | |
| impl Default for WrapWidth { | |
| fn default() -> Self { | |
| WrapWidth::None | |
| } | |
| } | |
| impl WrapWidth { | |
| fn differs_in_kind(self, other: WrapWidth) -> bool { | |
| use self::WrapWidth::*; | |
| match (self, other) { | |
| (None, None) | (Bytes(_), Bytes(_)) | (Width(_), Width(_)) => false, | |
| _else => true, | |
| } | |
| } | |
| } | |
| /// A range to be rewrapped. | |
| type Task = Interval; | |
| /// Tracks state related to visual lines. | |
| #[derive(Default)] | |
| pub(crate) struct Lines { | |
| breaks: Breaks, | |
| wrap: WrapWidth, | |
| /// Aka the 'frontier'; ranges of lines that still need to be wrapped. | |
| work: Vec<Task>, | |
| } | |
| pub(crate) struct VisualLine { | |
| pub(crate) interval: Interval, | |
| /// The logical line number for this line. Only present when this is the | |
| /// first visual line in a logical line. | |
| pub(crate) line_num: Option<usize>, | |
| } | |
| impl VisualLine { | |
| fn new<I: Into<Interval>, L: Into<Option<usize>>>(iv: I, line: L) -> Self { | |
| VisualLine { interval: iv.into(), line_num: line.into() } | |
| } | |
| } | |
| /// Describes what has changed after a batch of word wrapping; this is used | |
| /// for minimal invalidation. | |
| pub(crate) struct InvalLines { | |
| pub(crate) start_line: usize, | |
| pub(crate) inval_count: usize, | |
| pub(crate) new_count: usize, | |
| } | |
| /// Detailed information about changes to linebreaks, used to generate | |
| /// invalidation information. The logic behind invalidation is different | |
| /// depending on whether we're updating breaks after an edit, or continuing | |
| /// a bulk rewrapping task. This is shared between the two cases, and contains | |
| /// the information relevant to both of them. | |
| struct WrapSummary { | |
| start_line: usize, | |
| /// Total number of invalidated lines; this is meaningless in the after_edit case. | |
| inval_count: usize, | |
| /// The total number of new (hard + soft) breaks in the wrapped region. | |
| new_count: usize, | |
| /// The number of new soft breaks. | |
| new_soft: usize, | |
| } | |
| impl Lines { | |
| pub(crate) fn set_wrap_width(&mut self, text: &Rope, wrap: WrapWidth) { | |
| self.work.clear(); | |
| self.add_task(0..text.len()); | |
| if self.breaks.is_empty() || self.wrap.differs_in_kind(wrap) { | |
| // we keep breaks while resizing, for more efficient invalidation | |
| self.breaks = Breaks::new_no_break(text.len()); | |
| } | |
| self.wrap = wrap; | |
| } | |
| fn add_task<T: Into<Interval>>(&mut self, iv: T) { | |
| let iv = iv.into(); | |
| if iv.is_empty() { | |
| return; | |
| } | |
| // keep everything that doesn't intersect. merge things that do. | |
| let split_idx = match self.work.iter().position(|&t| !t.intersect(iv).is_empty()) { | |
| Some(idx) => idx, | |
| None => { | |
| self.work.push(iv); | |
| return; | |
| } | |
| }; | |
| let to_update = self.work.split_off(split_idx); | |
| let mut new_task = Some(iv); | |
| for t in &to_update { | |
| match new_task.take() { | |
| Some(new) if !t.intersect(new).is_empty() => new_task = Some(t.union(new)), | |
| Some(new) => { | |
| self.work.push(new); | |
| self.work.push(*t); | |
| } | |
| None => self.work.push(*t), | |
| } | |
| } | |
| if let Some(end) = new_task.take() { | |
| self.work.push(end); | |
| } | |
| } | |
| pub(crate) fn is_converged(&self) -> bool { | |
| self.wrap == WrapWidth::None || self.work.is_empty() | |
| } | |
| /// Returns `true` if this interval is part of an incomplete task. | |
| pub(crate) fn interval_needs_wrap(&self, iv: Interval) -> bool { | |
| self.work.iter().any(|t| !t.intersect(iv).is_empty()) | |
| } | |
| pub(crate) fn visual_line_of_offset(&self, text: &Rope, offset: usize) -> usize { | |
| let mut line = text.line_of_offset(offset); | |
| if self.wrap != WrapWidth::None { | |
| line += self.breaks.convert_metrics::<BreaksBaseMetric, BreaksMetric>(offset) | |
| } | |
| line | |
| } | |
| /// Returns the byte offset corresponding to the line `line`. | |
| pub(crate) fn offset_of_visual_line(&self, text: &Rope, line: usize) -> usize { | |
| match self.wrap { | |
| WrapWidth::None => { | |
| // sanitize input | |
| let line = line.min(text.measure::<LinesMetric>() + 1); | |
| text.offset_of_line(line) | |
| } | |
| _ => { | |
| let mut cursor = MergedBreaks::new(text, &self.breaks); | |
| cursor.offset_of_line(line) | |
| } | |
| } | |
| } | |
| /// Returns an iterator over [`VisualLine`]s, starting at (and including) | |
| /// `start_line`. | |
| pub(crate) fn iter_lines<'a>( | |
| &'a self, | |
| text: &'a Rope, | |
| start_line: usize, | |
| ) -> impl Iterator<Item = VisualLine> + 'a { | |
| let mut cursor = MergedBreaks::new(text, &self.breaks); | |
| let offset = cursor.offset_of_line(start_line); | |
| let logical_line = text.line_of_offset(offset) + 1; | |
| cursor.set_offset(offset); | |
| VisualLines { offset, cursor, len: text.len(), logical_line, eof: false } | |
| } | |
| /// Returns the next task, prioritizing the currently visible region. | |
| /// Does not modify the task list; this is done after the task runs. | |
| fn get_next_task(&self, visible_offset: usize) -> Option<Task> { | |
| // the first task t where t.end > visible_offset is the only task | |
| // that might contain the visible region. | |
| self.work | |
| .iter() | |
| .find(|t| t.end > visible_offset) | |
| .map(|t| Task::new(t.start.max(visible_offset), t.end)) | |
| .or(self.work.last().cloned()) | |
| } | |
| fn update_tasks_after_wrap<T: Into<Interval>>(&mut self, wrapped_iv: T) { | |
| if self.work.is_empty() { | |
| return; | |
| } | |
| let wrapped_iv = wrapped_iv.into(); | |
| let mut work = Vec::new(); | |
| for task in &self.work { | |
| if task.is_before(wrapped_iv.start) || task.is_after(wrapped_iv.end) { | |
| work.push(*task); | |
| continue; | |
| } | |
| if wrapped_iv.start > task.start { | |
| work.push(task.prefix(wrapped_iv)); | |
| } | |
| if wrapped_iv.end < task.end { | |
| work.push(task.suffix(wrapped_iv)); | |
| } | |
| } | |
| self.work = work; | |
| } | |
| /// Adjust offsets for any tasks after an edit. | |
| fn patchup_tasks<T: Into<Interval>>(&mut self, iv: T, new_len: usize) { | |
| let iv = iv.into(); | |
| let mut new_work = Vec::new(); | |
| for task in &self.work { | |
| if task.is_before(iv.start) { | |
| new_work.push(*task); | |
| } else if task.contains(iv.start) { | |
| let head = task.prefix(iv); | |
| let tail_end = iv.start.max((task.end + new_len).saturating_sub(iv.size())); | |
| let tail = Interval::new(iv.start, tail_end); | |
| new_work.push(head); | |
| new_work.push(tail); | |
| } else { | |
| new_work.push(task.translate_neg(iv.size()).translate(new_len)); | |
| } | |
| } | |
| new_work.retain(|iv| !iv.is_empty()); | |
| self.work.clear(); | |
| for task in new_work { | |
| if let Some(prev) = self.work.last_mut() { | |
| if prev.end >= task.start { | |
| *prev = prev.union(task); | |
| continue; | |
| } | |
| } | |
| self.work.push(task); | |
| } | |
| } | |
| /// Do a chunk of wrap work, if any exists. | |
| pub(crate) fn rewrap_chunk( | |
| &mut self, | |
| text: &Rope, | |
| width_cache: &mut WidthCache, | |
| client: &Client, | |
| _spans: &Spans<Style>, | |
| visible_lines: Range<usize>, | |
| ) -> Option<InvalLines> { | |
| if self.is_converged() { | |
| None | |
| } else { | |
| let summary = self.do_wrap_task(text, width_cache, client, visible_lines, None); | |
| let WrapSummary { start_line, inval_count, new_count, .. } = summary; | |
| Some(InvalLines { start_line, inval_count, new_count }) | |
| } | |
| } | |
| /// Updates breaks after an edit. Returns `InvalLines`, for minimal invalidation, | |
| /// when possible. | |
| pub(crate) fn after_edit( | |
| &mut self, | |
| text: &Rope, | |
| old_text: &Rope, | |
| delta: &RopeDelta, | |
| width_cache: &mut WidthCache, | |
| client: &Client, | |
| visible_lines: Range<usize>, | |
| ) -> Option<InvalLines> { | |
| let (iv, newlen) = delta.summary(); | |
| let logical_start_line = text.line_of_offset(iv.start); | |
| let old_logical_end_line = old_text.line_of_offset(iv.end) + 1; | |
| let new_logical_end_line = text.line_of_offset(iv.start + newlen) + 1; | |
| let old_logical_end_offset = old_text.offset_of_line(old_logical_end_line); | |
| let old_hard_count = old_logical_end_line - logical_start_line; | |
| let new_hard_count = new_logical_end_line - logical_start_line; | |
| //TODO: we should be able to avoid wrapping the whole para in most cases, | |
| // but the logic is trickier. | |
| let prev_break = text.offset_of_line(logical_start_line); | |
| let next_hard_break = text.offset_of_line(new_logical_end_line); | |
| // count the soft breaks in the region we will rewrap, before we update them. | |
| let inval_soft = | |
| self.breaks.convert_metrics::<BreaksBaseMetric, BreaksMetric>(old_logical_end_offset) | |
| - self.breaks.convert_metrics::<BreaksBaseMetric, BreaksMetric>(prev_break); | |
| // update soft breaks, adding empty spans in the edited region | |
| let mut builder = BreakBuilder::new(); | |
| builder.add_no_break(newlen); | |
| self.breaks.edit(iv, builder.build()); | |
| self.patchup_tasks(iv, newlen); | |
| if self.wrap == WrapWidth::None { | |
| return Some(InvalLines { | |
| start_line: logical_start_line, | |
| inval_count: old_hard_count, | |
| new_count: new_hard_count, | |
| }); | |
| } | |
| let new_task = prev_break..next_hard_break; | |
| self.add_task(new_task); | |
| // possible if the whole buffer is deleted, e.g | |
| if !self.work.is_empty() { | |
| let summary = self.do_wrap_task(text, width_cache, client, visible_lines, None); | |
| let WrapSummary { start_line, new_soft, .. } = summary; | |
| // if we haven't converged after this update we can't do minimal invalidation | |
| // because we don't have complete knowledge of the new breaks state. | |
| if self.is_converged() { | |
| let inval_count = old_hard_count + inval_soft; | |
| let new_count = new_hard_count + new_soft; | |
| Some(InvalLines { start_line, new_count, inval_count }) | |
| } else { | |
| None | |
| } | |
| } else { | |
| None | |
| } | |
| } | |
| fn do_wrap_task( | |
| &mut self, | |
| text: &Rope, | |
| width_cache: &mut WidthCache, | |
| client: &Client, | |
| visible_lines: Range<usize>, | |
| max_lines: Option<usize>, | |
| ) -> WrapSummary { | |
| use self::WrapWidth::*; | |
| let _t = trace_block("Lines::do_wrap_task", &["core"]); | |
| // 'line' is a poor unit here; could do some fancy Duration thing? | |
| const MAX_LINES_PER_BATCH: usize = 500; | |
| let mut cursor = MergedBreaks::new(text, &self.breaks); | |
| let visible_off = cursor.offset_of_line(visible_lines.start); | |
| let logical_off = text.offset_of_line(text.line_of_offset(visible_off)); | |
| // task.start is a hard break; task.end is a boundary or EOF. | |
| let task = self.get_next_task(logical_off).unwrap(); | |
| cursor.set_offset(task.start); | |
| debug_assert_eq!(cursor.offset, task.start, "task_start must be valid offset"); | |
| let mut ctx = match self.wrap { | |
| Bytes(b) => RewrapCtx::new(text, &CodepointMono, b as f64, width_cache, task.start), | |
| Width(w) => RewrapCtx::new(text, client, w, width_cache, task.start), | |
| None => unreachable!(), | |
| }; | |
| let start_line = cursor.cur_line; | |
| let max_lines = max_lines.unwrap_or(MAX_LINES_PER_BATCH); | |
| // always wrap at least a screen worth of lines (unless we converge earlier) | |
| let batch_size = max_lines.max(visible_lines.end - visible_lines.start); | |
| let mut builder = BreakBuilder::new(); | |
| let mut lines_wrapped = 0; | |
| let mut pos = task.start; | |
| let mut old_next_maybe = cursor.next(); | |
| loop { | |
| if let Some(new_next) = ctx.wrap_one_line(pos) { | |
| while let Some(old_next) = old_next_maybe { | |
| if old_next >= new_next { | |
| break; // just advance old cursor and continue | |
| } | |
| old_next_maybe = cursor.next(); | |
| } | |
| let is_hard = cursor.offset == new_next && cursor.is_hard_break(); | |
| if is_hard { | |
| builder.add_no_break(new_next - pos); | |
| } else { | |
| builder.add_break(new_next - pos); | |
| } | |
| lines_wrapped += 1; | |
| pos = new_next; | |
| if pos == task.end || (lines_wrapped > batch_size && is_hard) { | |
| break; | |
| } | |
| } else { | |
| // EOF | |
| builder.add_no_break(text.len() - pos); | |
| break; | |
| } | |
| } | |
| let breaks = builder.build(); | |
| let end = task.start + breaks.len(); | |
| // this is correct *only* when an edit has not occured. | |
| let inval_soft = self.breaks.convert_metrics::<BreaksBaseMetric, BreaksMetric>(end) | |
| - self.breaks.convert_metrics::<BreaksBaseMetric, BreaksMetric>(task.start); | |
| let hard_count = 1 + text.line_of_offset(end) - text.line_of_offset(task.start); | |
| let inval_count = inval_soft + hard_count; | |
| let new_soft = breaks.measure::<BreaksMetric>(); | |
| let new_count = new_soft + hard_count; | |
| let iv = Interval::new(task.start, end); | |
| self.breaks.edit(iv, breaks); | |
| self.update_tasks_after_wrap(iv); | |
| WrapSummary { start_line, inval_count, new_count, new_soft } | |
| } | |
| #[cfg(test)] | |
| fn for_testing(text: &Rope, wrap: WrapWidth) -> Lines { | |
| let mut lines = Lines::default(); | |
| lines.set_wrap_width(text, wrap); | |
| lines | |
| } | |
| #[cfg(test)] | |
| fn rewrap_all(&mut self, text: &Rope, client: &Client, width_cache: &mut WidthCache) { | |
| if !self.is_converged() { | |
| self.do_wrap_task(text, width_cache, client, 0..10, Some(usize::max_value())); | |
| } | |
| } | |
| } | |
| /// A potential opportunity to insert a break. In this representation, the widths | |
| /// have been requested (in a batch request) but are not necessarily known until | |
| /// the request is issued. | |
| struct PotentialBreak { | |
| /// The offset within the text of the end of the word. | |
| pos: usize, | |
| /// A token referencing the width of the word, to be resolved in the width cache. | |
| tok: Token, | |
| /// Whether the break is a hard break or a soft break. | |
| hard: bool, | |
| } | |
| /// State for a rewrap in progress | |
| struct RewrapCtx<'a> { | |
| text: &'a Rope, | |
| lb_cursor: LineBreakCursor<'a>, | |
| lb_cursor_pos: usize, | |
| width_cache: &'a mut WidthCache, | |
| client: &'a dyn WidthMeasure, | |
| pot_breaks: Vec<PotentialBreak>, | |
| /// Index within `pot_breaks` | |
| pot_break_ix: usize, | |
| max_width: f64, | |
| } | |
| // This constant should be tuned so that the RPC takes about 1ms. Less than that, | |
| // RPC overhead becomes significant. More than that, interactivity suffers. | |
| const MAX_POT_BREAKS: usize = 10_000; | |
| impl<'a> RewrapCtx<'a> { | |
| fn new( | |
| text: &'a Rope, | |
| //_style_spans: &Spans<Style>, client: &'a T, | |
| client: &'a dyn WidthMeasure, | |
| max_width: f64, | |
| width_cache: &'a mut WidthCache, | |
| start: usize, | |
| ) -> RewrapCtx<'a> { | |
| let lb_cursor_pos = start; | |
| let lb_cursor = LineBreakCursor::new(text, start); | |
| RewrapCtx { | |
| text, | |
| lb_cursor, | |
| lb_cursor_pos, | |
| width_cache, | |
| client, | |
| pot_breaks: Vec::new(), | |
| pot_break_ix: 0, | |
| max_width, | |
| } | |
| } | |
| fn refill_pot_breaks(&mut self) { | |
| let mut req = self.width_cache.batch_req(); | |
| self.pot_breaks.clear(); | |
| self.pot_break_ix = 0; | |
| let mut pos = self.lb_cursor_pos; | |
| while pos < self.text.len() && self.pot_breaks.len() < MAX_POT_BREAKS { | |
| let (next, hard) = self.lb_cursor.next(); | |
| let word = self.text.slice_to_cow(pos..next); | |
| let tok = req.request(N_RESERVED_STYLES, &word); | |
| pos = next; | |
| self.pot_breaks.push(PotentialBreak { pos, tok, hard }); | |
| } | |
| req.resolve_pending(self.client).unwrap(); | |
| self.lb_cursor_pos = pos; | |
| } | |
| /// Compute the next break, assuming `start` is a valid break. | |
| /// | |
| /// Invariant: `start` corresponds to the start of the word referenced by `pot_break_ix`. | |
| fn wrap_one_line(&mut self, start: usize) -> Option<usize> { | |
| let mut line_width = 0.0; | |
| let mut pos = start; | |
| while pos < self.text.len() { | |
| if self.pot_break_ix >= self.pot_breaks.len() { | |
| self.refill_pot_breaks(); | |
| } | |
| let pot_break = &self.pot_breaks[self.pot_break_ix]; | |
| let width = self.width_cache.resolve(pot_break.tok); | |
| if !pot_break.hard { | |
| if line_width == 0.0 && width >= self.max_width { | |
| // we don't care about soft breaks at EOF | |
| if pot_break.pos == self.text.len() { | |
| return None; | |
| } | |
| self.pot_break_ix += 1; | |
| return Some(pot_break.pos); | |
| } | |
| line_width += width; | |
| if line_width > self.max_width { | |
| return Some(pos); | |
| } | |
| self.pot_break_ix += 1; | |
| pos = pot_break.pos; | |
| } else if line_width != 0. && width + line_width > self.max_width { | |
| // if this is a hard break but we would have broken at the previous | |
| // pos otherwise, we still break at the previous pos. | |
| return Some(pos); | |
| } else { | |
| self.pot_break_ix += 1; | |
| return Some(pot_break.pos); | |
| } | |
| } | |
| None | |
| } | |
| } | |
| struct LineBreakCursor<'a> { | |
| inner: Cursor<'a, RopeInfo>, | |
| lb_iter: LineBreakLeafIter, | |
| last_byte: u8, | |
| } | |
| impl<'a> LineBreakCursor<'a> { | |
| fn new(text: &'a Rope, pos: usize) -> LineBreakCursor<'a> { | |
| let inner = Cursor::new(text, pos); | |
| let lb_iter = match inner.get_leaf() { | |
| Some((s, offset)) => LineBreakLeafIter::new(s.as_str(), offset), | |
| _ => LineBreakLeafIter::default(), | |
| }; | |
| LineBreakCursor { inner, lb_iter, last_byte: 0 } | |
| } | |
| // position and whether break is hard; up to caller to stop calling after EOT | |
| fn next(&mut self) -> (usize, bool) { | |
| let mut leaf = self.inner.get_leaf(); | |
| loop { | |
| match leaf { | |
| Some((s, offset)) => { | |
| let (next, hard) = self.lb_iter.next(s.as_str()); | |
| if next < s.len() { | |
| return (self.inner.pos() - offset + next, hard); | |
| } | |
| if !s.is_empty() { | |
| self.last_byte = s.as_bytes()[s.len() - 1]; | |
| } | |
| leaf = self.inner.next_leaf(); | |
| } | |
| // A little hacky but only reports last break as hard if final newline | |
| None => return (self.inner.pos(), self.last_byte == b'\n'), | |
| } | |
| } | |
| } | |
| } | |
| struct VisualLines<'a> { | |
| cursor: MergedBreaks<'a>, | |
| offset: usize, | |
| /// The current logical line number. | |
| logical_line: usize, | |
| len: usize, | |
| eof: bool, | |
| } | |
| impl<'a> Iterator for VisualLines<'a> { | |
| type Item = VisualLine; | |
| fn next(&mut self) -> Option<VisualLine> { | |
| let line_num = if self.cursor.is_hard_break() { Some(self.logical_line) } else { None }; | |
| let next_end_bound = match self.cursor.next() { | |
| Some(b) => b, | |
| None if self.eof => return None, | |
| _else => { | |
| self.eof = true; | |
| self.len | |
| } | |
| }; | |
| let result = VisualLine::new(self.offset..next_end_bound, line_num); | |
| if self.cursor.is_hard_break() { | |
| self.logical_line += 1; | |
| } | |
| self.offset = next_end_bound; | |
| Some(result) | |
| } | |
| } | |
| /// A cursor over both hard and soft breaks. Hard breaks are retrieved from | |
| /// the rope; the soft breaks are stored independently; this interleaves them. | |
| /// | |
| /// # Invariants: | |
| /// | |
| /// `self.offset` is always a valid break in one of the cursors, unless | |
| /// at 0 or EOF. | |
| /// | |
| /// `self.offset == self.text.pos().min(self.soft.pos())`. | |
| struct MergedBreaks<'a> { | |
| text: Cursor<'a, RopeInfo>, | |
| soft: Cursor<'a, BreaksInfo>, | |
| offset: usize, | |
| /// Starting from zero, how many calls to `next` to get to `self.offset`? | |
| cur_line: usize, | |
| total_lines: usize, | |
| /// Total length, in base units | |
| len: usize, | |
| } | |
| impl<'a> Iterator for MergedBreaks<'a> { | |
| type Item = usize; | |
| fn next(&mut self) -> Option<usize> { | |
| if self.text.pos() == self.offset && !self.at_eof() { | |
| // don't iterate past EOF, or we can't get the leaf and check for \n | |
| self.text.next::<LinesMetric>(); | |
| } | |
| if self.soft.pos() == self.offset { | |
| self.soft.next::<BreaksMetric>(); | |
| } | |
| let prev_off = self.offset; | |
| self.offset = self.text.pos().min(self.soft.pos()); | |
| let eof_without_newline = self.offset > 0 && self.at_eof() && self.eof_without_newline(); | |
| if self.offset == prev_off || eof_without_newline { | |
| None | |
| } else { | |
| self.cur_line += 1; | |
| Some(self.offset) | |
| } | |
| } | |
| } | |
| // arrived at this by just trying out a bunch of values ¯\_(ツ)_/¯ | |
| /// how far away a line can be before we switch to a binary search | |
| const MAX_LINEAR_DIST: usize = 20; | |
| impl<'a> MergedBreaks<'a> { | |
| fn new(text: &'a Rope, breaks: &'a Breaks) -> Self { | |
| debug_assert_eq!(text.len(), breaks.len()); | |
| let text = Cursor::new(text, 0); | |
| let soft = Cursor::new(breaks, 0); | |
| let total_lines = | |
| text.root().measure::<LinesMetric>() + soft.root().measure::<BreaksMetric>() + 1; | |
| let len = text.total_len(); | |
| MergedBreaks { text, soft, offset: 0, cur_line: 0, total_lines, len } | |
| } | |
| /// Sets the `self.offset` to the first valid break immediately at or preceding `offset`, | |
| /// and restores invariants. | |
| fn set_offset(&mut self, offset: usize) { | |
| self.text.set(offset); | |
| self.soft.set(offset); | |
| if offset > 0 { | |
| if self.text.at_or_prev::<LinesMetric>().is_none() { | |
| self.text.set(0); | |
| } | |
| if self.soft.at_or_prev::<BreaksMetric>().is_none() { | |
| self.soft.set(0); | |
| } | |
| } | |
| // self.offset should be at the first valid break immediately preceding `offset`, or 0. | |
| // the position of the non-break cursor should be > than that of the break cursor, or EOF. | |
| match self.text.pos().cmp(&self.soft.pos()) { | |
| Ordering::Less => { | |
| self.text.next::<LinesMetric>(); | |
| } | |
| Ordering::Greater => { | |
| self.soft.next::<BreaksMetric>(); | |
| } | |
| Ordering::Equal => assert!(self.text.pos() == 0), | |
| } | |
| self.offset = self.text.pos().min(self.soft.pos()); | |
| self.cur_line = merged_line_of_offset(self.text.root(), self.soft.root(), self.offset); | |
| } | |
| fn offset_of_line(&mut self, line: usize) -> usize { | |
| match line { | |
| 0 => 0, | |
| l if l >= self.total_lines => self.text.total_len(), | |
| l if l == self.cur_line => self.offset, | |
| l if l > self.cur_line && l - self.cur_line < MAX_LINEAR_DIST => { | |
| self.offset_of_line_linear(l) | |
| } | |
| other => self.offset_of_line_bsearch(other), | |
| } | |
| } | |
| fn offset_of_line_linear(&mut self, line: usize) -> usize { | |
| assert!(line > self.cur_line); | |
| let dist = line - self.cur_line; | |
| self.nth(dist - 1).unwrap_or(self.len) | |
| } | |
| fn offset_of_line_bsearch(&mut self, line: usize) -> usize { | |
| let mut range = 0..self.len; | |
| loop { | |
| let pivot = range.start + (range.end - range.start) / 2; | |
| self.set_offset(pivot); | |
| match self.cur_line { | |
| l if l == line => break self.offset, | |
| l if l > line => range = range.start..pivot, | |
| l if line - l > MAX_LINEAR_DIST => range = pivot..range.end, | |
| _else => break self.offset_of_line_linear(line), | |
| } | |
| } | |
| } | |
| fn is_hard_break(&self) -> bool { | |
| self.offset == self.text.pos() | |
| } | |
| fn at_eof(&self) -> bool { | |
| self.offset == self.len | |
| } | |
| fn eof_without_newline(&mut self) -> bool { | |
| debug_assert!(self.at_eof()); | |
| self.text.set(self.len); | |
| self.text.get_leaf().map(|(l, _)| l.as_bytes().last() != Some(&b'\n')).unwrap() | |
| } | |
| } | |
| fn merged_line_of_offset(text: &Rope, soft: &Breaks, offset: usize) -> usize { | |
| text.convert_metrics::<BaseMetric, LinesMetric>(offset) | |
| + soft.convert_metrics::<BreaksBaseMetric, BreaksMetric>(offset) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::*; | |
| use std::borrow::Cow; | |
| use std::iter; | |
| use xi_rpc::test_utils::DummyPeer; | |
| fn make_lines(text: &Rope, width: f64) -> Lines { | |
| let client = Client::new(Box::new(DummyPeer)); | |
| let mut width_cache = WidthCache::new(); | |
| let wrap = WrapWidth::Bytes(width as usize); | |
| let mut lines = Lines::for_testing(text, wrap); | |
| lines.rewrap_all(text, &client, &mut width_cache); | |
| lines | |
| } | |
| fn render_breaks<'a>(text: &'a Rope, lines: &Lines) -> Vec<Cow<'a, str>> { | |
| let result = lines.iter_lines(text, 0).map(|l| text.slice_to_cow(l.interval)).collect(); | |
| result | |
| } | |
| fn debug_breaks<'a>(text: &'a Rope, width: f64) -> Vec<Cow<'a, str>> { | |
| let lines = make_lines(text, width); | |
| render_breaks(text, &lines) | |
| } | |
| #[test] | |
| fn column_breaks_basic() { | |
| let text: Rope = "every wordthing should getits own".into(); | |
| let result = debug_breaks(&text, 8.0); | |
| assert_eq!(result, vec!["every ", "wordthing ", "should ", "getits ", "own",]); | |
| } | |
| #[test] | |
| fn column_breaks_trailing_newline() { | |
| let text: Rope = "every wordthing should getits ow\n".into(); | |
| let result = debug_breaks(&text, 8.0); | |
| assert_eq!(result, vec!["every ", "wordthing ", "should ", "getits ", "ow\n", "",]); | |
| } | |
| #[test] | |
| fn soft_before_hard() { | |
| let text: Rope = "create abreak between THESE TWO\nwords andbreakcorrectlyhere\nplz".into(); | |
| let result = debug_breaks(&text, 4.0); | |
| assert_eq!( | |
| result, | |
| vec![ | |
| "create ", | |
| "abreak ", | |
| "between ", | |
| "THESE ", | |
| "TWO\n", | |
| "words ", | |
| "andbreakcorrectlyhere\n", | |
| "plz", | |
| ] | |
| ); | |
| } | |
| #[test] | |
| fn column_breaks_hard_soft() { | |
| let text: Rope = "so\nevery wordthing should getits own".into(); | |
| let result = debug_breaks(&text, 4.0); | |
| assert_eq!(result, vec!["so\n", "every ", "wordthing ", "should ", "getits ", "own",]); | |
| } | |
| #[test] | |
| fn empty_file() { | |
| let text: Rope = "".into(); | |
| let result = debug_breaks(&text, 4.0); | |
| assert_eq!(result, vec![""]); | |
| } | |
| #[test] | |
| fn dont_break_til_i_tell_you() { | |
| let text: Rope = "thisis_longerthan_our_break_width".into(); | |
| let result = debug_breaks(&text, 12.0); | |
| assert_eq!(result, vec!["thisis_longerthan_our_break_width"]); | |
| } | |
| #[test] | |
| fn break_now_though() { | |
| let text: Rope = "thisis_longerthan_our_break_width hi".into(); | |
| let result = debug_breaks(&text, 12.0); | |
| assert_eq!(result, vec!["thisis_longerthan_our_break_width ", "hi"]); | |
| } | |
| #[test] | |
| fn newlines() { | |
| let text: Rope = "\n\n".into(); | |
| let result = debug_breaks(&text, 4.0); | |
| assert_eq!(result, vec!["\n", "\n", ""]); | |
| } | |
| #[test] | |
| fn newline_eof() { | |
| let text: Rope = "hello\n".into(); | |
| let result = debug_breaks(&text, 4.0); | |
| assert_eq!(result, vec!["hello\n", ""]); | |
| } | |
| #[test] | |
| fn no_newline_eof() { | |
| let text: Rope = "hello".into(); | |
| let result = debug_breaks(&text, 4.0); | |
| assert_eq!(result, vec!["hello"]); | |
| } | |
| #[test] | |
| fn merged_offset() { | |
| let text: Rope = "a quite\nshort text".into(); | |
| let mut builder = BreakBuilder::new(); | |
| builder.add_break(2); | |
| builder.add_no_break(text.len() - 2); | |
| let breaks = builder.build(); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, 0), 0); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, 1), 0); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, 2), 1); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, 5), 1); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, 5), 1); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, 9), 2); | |
| assert_eq!(merged_line_of_offset(&text, &breaks, text.len()), 2); | |
| let text: Rope = "a quite\nshort tex\n".into(); | |
| // trailing newline increases total count | |
| assert_eq!(merged_line_of_offset(&text, &breaks, text.len()), 3); | |
| } | |
| #[test] | |
| fn bsearch_equivalence() { | |
| let text: Rope = | |
| iter::repeat("this is a line with some text in it, which is not unusual\n") | |
| .take(1000) | |
| .collect::<String>() | |
| .into(); | |
| let lines = make_lines(&text, 30.); | |
| let mut linear = MergedBreaks::new(&text, &lines.breaks); | |
| let mut binary = MergedBreaks::new(&text, &lines.breaks); | |
| // skip zero because these two impls don't handle edge cases | |
| for i in 1..1000 { | |
| linear.set_offset(0); | |
| binary.set_offset(0); | |
| assert_eq!( | |
| linear.offset_of_line_linear(i), | |
| binary.offset_of_line_bsearch(i), | |
| "line {}", | |
| i | |
| ); | |
| } | |
| } | |
| #[test] | |
| fn merge_cursor_no_breaks() { | |
| let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); | |
| // first with no breaks | |
| let breaks = Breaks::new_no_break(text.len()); | |
| let mut cursor = MergedBreaks::new(&text, &breaks); | |
| assert_eq!(cursor.offset, 0); | |
| assert_eq!(cursor.cur_line, 0); | |
| assert_eq!(cursor.len, text.len()); | |
| assert_eq!(cursor.total_lines, 4); | |
| assert!(cursor.is_hard_break()); | |
| assert_eq!(cursor.next(), Some(5)); | |
| assert_eq!(cursor.cur_line, 1); | |
| assert_eq!(cursor.offset, 5); | |
| assert_eq!(cursor.text.pos(), 5); | |
| assert_eq!(cursor.soft.pos(), text.len()); | |
| assert!(cursor.is_hard_break()); | |
| assert_eq!(cursor.next(), Some(14)); | |
| assert_eq!(cursor.cur_line, 2); | |
| assert_eq!(cursor.offset, 14); | |
| assert_eq!(cursor.text.pos(), 14); | |
| assert_eq!(cursor.soft.pos(), text.len()); | |
| assert!(cursor.is_hard_break()); | |
| assert_eq!(cursor.next(), Some(30)); | |
| assert_eq!(cursor.next(), None); | |
| } | |
| #[test] | |
| fn merge_cursor_breaks() { | |
| let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); | |
| let mut builder = BreakBuilder::new(); | |
| builder.add_break(8); | |
| builder.add_break(3); | |
| builder.add_no_break(text.len() - (8 + 3)); | |
| let breaks = builder.build(); | |
| let mut cursor = MergedBreaks::new(&text, &breaks); | |
| assert_eq!(cursor.offset, 0); | |
| assert_eq!(cursor.cur_line, 0); | |
| assert_eq!(cursor.len, text.len()); | |
| assert_eq!(cursor.total_lines, 6); | |
| assert_eq!(cursor.next(), Some(5)); | |
| assert_eq!(cursor.cur_line, 1); | |
| assert_eq!(cursor.offset, 5); | |
| assert_eq!(cursor.text.pos(), 5); | |
| assert_eq!(cursor.soft.pos(), 8); | |
| assert!(cursor.is_hard_break()); | |
| assert_eq!(cursor.next(), Some(8)); | |
| assert_eq!(cursor.cur_line, 2); | |
| assert_eq!(cursor.offset, 8); | |
| assert_eq!(cursor.text.pos(), 14); | |
| assert_eq!(cursor.soft.pos(), 8); | |
| assert!(!cursor.is_hard_break()); | |
| assert_eq!(cursor.next(), Some(11)); | |
| assert_eq!(cursor.cur_line, 3); | |
| assert_eq!(cursor.offset, 11); | |
| assert_eq!(cursor.text.pos(), 14); | |
| assert_eq!(cursor.soft.pos(), 11); | |
| assert!(!cursor.is_hard_break()); | |
| assert_eq!(cursor.next(), Some(14)); | |
| assert_eq!(cursor.cur_line, 4); | |
| assert_eq!(cursor.offset, 14); | |
| assert_eq!(cursor.text.pos(), 14); | |
| assert_eq!(cursor.soft.pos(), text.len()); | |
| assert!(cursor.is_hard_break()); | |
| } | |
| #[test] | |
| fn set_offset() { | |
| let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); | |
| let lines = make_lines(&text, 2.); | |
| let mut merged = MergedBreaks::new(&text, &lines.breaks); | |
| assert_eq!(merged.total_lines, 10); | |
| let check_props = |m: &MergedBreaks, line, off, softpos, hardpos| { | |
| assert_eq!(m.cur_line, line); | |
| assert_eq!(m.offset, off); | |
| assert_eq!(m.soft.pos(), softpos); | |
| assert_eq!(m.text.pos(), hardpos); | |
| }; | |
| merged.next(); | |
| check_props(&merged, 1, 5, 8, 5); | |
| merged.set_offset(0); | |
| check_props(&merged, 0, 0, 0, 0); | |
| merged.set_offset(5); | |
| check_props(&merged, 1, 5, 8, 5); | |
| merged.set_offset(0); | |
| merged.set_offset(6); | |
| check_props(&merged, 1, 5, 8, 5); | |
| merged.set_offset(9); | |
| check_props(&merged, 2, 8, 8, 14); | |
| merged.set_offset(text.len()); | |
| check_props(&merged, 9, 33, 33, 37); | |
| merged.set_offset(text.len() - 1); | |
| check_props(&merged, 9, 33, 33, 37); | |
| // but a trailing newline adds a line at EOF | |
| let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff ggg\n".into(); | |
| let lines = make_lines(&text, 2.); | |
| let mut merged = MergedBreaks::new(&text, &lines.breaks); | |
| assert_eq!(merged.total_lines, 11); | |
| merged.set_offset(text.len()); | |
| check_props(&merged, 10, 37, 37, 37); | |
| merged.set_offset(text.len() - 1); | |
| check_props(&merged, 9, 33, 33, 37); | |
| } | |
| #[test] | |
| fn test_break_at_linear_transition() { | |
| // do we handle the break at MAX_LINEAR_DIST correctly? | |
| let text = "a b c d e f g h i j k l m n o p q r s t u v w x ".into(); | |
| let lines = make_lines(&text, 1.); | |
| for offset in 0..text.len() { | |
| let line = lines.visual_line_of_offset(&text, offset); | |
| let line_offset = lines.offset_of_visual_line(&text, line); | |
| assert!(line_offset <= offset, "{} <= {} L{} O{}", line_offset, offset, line, offset); | |
| } | |
| } | |
| #[test] | |
| fn expected_soft_breaks() { | |
| let text = "a b c d ".into(); | |
| let mut text_cursor = Cursor::new(&text, text.len()); | |
| assert!(!text_cursor.is_boundary::<LinesMetric>()); | |
| let Lines { breaks, .. } = make_lines(&text, 1.); | |
| let mut cursor = Cursor::new(&breaks, 0); | |
| cursor.set(2); | |
| assert!(cursor.is_boundary::<BreaksMetric>()); | |
| cursor.set(4); | |
| assert!(cursor.is_boundary::<BreaksMetric>()); | |
| cursor.set(6); | |
| assert!(cursor.is_boundary::<BreaksMetric>()); | |
| cursor.set(8); | |
| assert!(!cursor.is_boundary::<BreaksMetric>()); | |
| cursor.set(0); | |
| let breaks = cursor.iter::<BreaksMetric>().collect::<Vec<_>>(); | |
| assert_eq!(breaks, vec![2, 4, 6]); | |
| } | |
| #[test] | |
| fn expected_soft_with_hard() { | |
| let text: Rope = "aa\nbb cc\ncc dd ee ff\ngggg".into(); | |
| let Lines { breaks, .. } = make_lines(&text, 2.); | |
| let mut cursor = Cursor::new(&breaks, 0); | |
| let breaks = cursor.iter::<BreaksMetric>().collect::<Vec<_>>(); | |
| assert_eq!(breaks, vec![6, 12, 15, 18]); | |
| } | |
| #[test] | |
| fn offset_to_line() { | |
| let text = "a b c d ".into(); | |
| let lines = make_lines(&text, 1.); | |
| let cursor = MergedBreaks::new(&text, &lines.breaks); | |
| assert_eq!(cursor.total_lines, 4); | |
| assert_eq!(lines.visual_line_of_offset(&text, 0), 0); | |
| assert_eq!(lines.visual_line_of_offset(&text, 1), 0); | |
| assert_eq!(lines.visual_line_of_offset(&text, 2), 1); | |
| assert_eq!(lines.visual_line_of_offset(&text, 3), 1); | |
| assert_eq!(lines.visual_line_of_offset(&text, 4), 2); | |
| assert_eq!(lines.visual_line_of_offset(&text, 5), 2); | |
| assert_eq!(lines.visual_line_of_offset(&text, 6), 3); | |
| assert_eq!(lines.visual_line_of_offset(&text, 7), 3); | |
| assert_eq!(lines.offset_of_visual_line(&text, 0), 0); | |
| assert_eq!(lines.offset_of_visual_line(&text, 1), 2); | |
| assert_eq!(lines.offset_of_visual_line(&text, 2), 4); | |
| assert_eq!(lines.offset_of_visual_line(&text, 3), 6); | |
| assert_eq!(lines.offset_of_visual_line(&text, 10), 8); | |
| for offset in 0..text.len() { | |
| let line = lines.visual_line_of_offset(&text, offset); | |
| let line_offset = lines.offset_of_visual_line(&text, 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 lines = make_lines(&text, 2.); | |
| let r: Vec<_> = | |
| lines.iter_lines(&text, 0).take(2).map(|l| text.slice_to_cow(l.interval)).collect(); | |
| assert_eq!(r, vec!["aaaa\n", "bb "]); | |
| let r: Vec<_> = | |
| lines.iter_lines(&text, 1).take(2).map(|l| text.slice_to_cow(l.interval)).collect(); | |
| assert_eq!(r, vec!["bb ", "bb "]); | |
| let r: Vec<_> = | |
| lines.iter_lines(&text, 3).take(3).map(|l| text.slice_to_cow(l.interval)).collect(); | |
| assert_eq!(r, vec!["cc\n", "cc ", "dddd "]); | |
| } | |
| #[test] | |
| fn line_numbers() { | |
| let text: Rope = "aaaa\nbb bb cc\ncc dddd eeee ff\nff gggg".into(); | |
| let lines = make_lines(&text, 2.); | |
| let nums: Vec<_> = lines.iter_lines(&text, 0).map(|l| l.line_num).collect(); | |
| assert_eq!( | |
| nums, | |
| vec![Some(1), Some(2), None, None, Some(3), None, None, None, Some(4), None] | |
| ); | |
| } | |
| fn make_ranges(ivs: &[Interval]) -> Vec<Range<usize>> { | |
| ivs.iter().map(|iv| iv.start..iv.end).collect() | |
| } | |
| #[test] | |
| fn update_frontier() { | |
| let mut lines = Lines::default(); | |
| lines.add_task(0..1000); | |
| lines.update_tasks_after_wrap(0..10); | |
| lines.update_tasks_after_wrap(50..100); | |
| assert_eq!(make_ranges(&lines.work), vec![10..50, 100..1000]); | |
| lines.update_tasks_after_wrap(200..1000); | |
| assert_eq!(make_ranges(&lines.work), vec![10..50, 100..200]); | |
| lines.add_task(300..400); | |
| assert_eq!(make_ranges(&lines.work), vec![10..50, 100..200, 300..400]); | |
| lines.add_task(250..350); | |
| assert_eq!(make_ranges(&lines.work), vec![10..50, 100..200, 250..400]); | |
| lines.add_task(60..450); | |
| assert_eq!(make_ranges(&lines.work), vec![10..50, 60..450]); | |
| } | |
| #[test] | |
| fn patchup_frontier() { | |
| let mut lines = Lines::default(); | |
| lines.add_task(0..100); | |
| assert_eq!(make_ranges(&lines.work), vec![0..100]); | |
| lines.patchup_tasks(20..30, 20); | |
| assert_eq!(make_ranges(&lines.work), vec![0..110]); | |
| // delete everything? | |
| lines.patchup_tasks(0..200, 0); | |
| assert_eq!(make_ranges(&lines.work), vec![]); | |
| lines.add_task(0..110); | |
| lines.patchup_tasks(0..30, 0); | |
| assert_eq!(make_ranges(&lines.work), vec![0..80]); | |
| lines.update_tasks_after_wrap(20..30); | |
| assert_eq!(make_ranges(&lines.work), vec![0..20, 30..80]); | |
| lines.patchup_tasks(10..40, 0); | |
| assert_eq!(make_ranges(&lines.work), vec![0..50]); | |
| lines.update_tasks_after_wrap(20..30); | |
| assert_eq!(make_ranges(&lines.work), vec![0..20, 30..50]); | |
| lines.patchup_tasks(10..10, 10); | |
| assert_eq!(make_ranges(&lines.work), vec![0..30, 40..60]); | |
| } | |
| } |