use crate::{Cursor, Doc, Selection};
const CONTEXT: usize = 32;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Quote {
pub exact: String,
pub prefix: String,
pub suffix: String,
}
impl Quote {
pub fn of(doc: &Doc, range: Selection) -> Option<Self> {
let flat = Flat::new(doc);
let (start, end) = range.clamp(doc).ordered();
let (start, end) = (flat.index(start)?, flat.index(end)?);
if start >= end {
return None;
}
let text = flat.text.as_str();
Some(Self {
exact: text[start..end].to_string(),
prefix: tail(&text[..start], CONTEXT).to_string(),
suffix: head(&text[end..], CONTEXT).to_string(),
})
}
pub fn find(&self, doc: &Doc, hint: Option<Selection>) -> Option<Selection> {
if self.exact.is_empty() {
return None;
}
let flat = Flat::new(doc);
let text = flat.text.as_str();
let near = hint.and_then(|hint| flat.index(hint.clamp(doc).ordered().0));
if let Some(at) = near
&& text
.get(at..)
.is_some_and(|rest| rest.starts_with(&self.exact))
{
return flat.selection(at, at + self.exact.len());
}
let at = occurrences(text, &self.exact).max_by_key(|&at| {
let end = at + self.exact.len();
let context =
common_tail(&text[..at], &self.prefix) + common_head(&text[end..], &self.suffix);
let distance = near.map_or(0, |near| near.abs_diff(at));
(context, std::cmp::Reverse(distance))
})?;
flat.selection(at, at + self.exact.len())
}
}
struct Flat {
text: String,
parts: Vec<(usize, Cursor)>,
}
impl Flat {
fn new(doc: &Doc) -> Self {
let mut text = String::new();
let mut parts = Vec::new();
for (ix, block) in doc.blocks.iter().enumerate() {
for part in block.parts() {
let Some(body) = block.text_at(part) else {
continue;
};
if !parts.is_empty() {
text.push('\n');
}
parts.push((text.len(), Cursor::new(ix, part, 0)));
text.push_str(&body.text);
}
}
Self { text, parts }
}
fn index(&self, cursor: Cursor) -> Option<usize> {
let at = self
.parts
.iter()
.position(|(_, start)| start.block == cursor.block && start.part == cursor.part)?;
let (start, _) = self.parts[at];
let end = self
.parts
.get(at + 1)
.map_or(self.text.len(), |(next, _)| next - 1);
Some((start + cursor.offset).min(end))
}
fn cursor(&self, at: usize) -> Option<Cursor> {
let (start, cursor) = self.parts.iter().rev().find(|(start, _)| *start <= at)?;
Some(Cursor {
offset: at - start,
..*cursor
})
}
fn selection(&self, start: usize, end: usize) -> Option<Selection> {
Some(Selection::new(self.cursor(start)?, self.cursor(end)?))
}
}
fn occurrences<'a>(text: &'a str, exact: &'a str) -> impl Iterator<Item = usize> + 'a {
text.char_indices()
.map(|(at, _)| at)
.filter(move |&at| text[at..].starts_with(exact))
}
fn tail(text: &str, count: usize) -> &str {
let at = text
.char_indices()
.rev()
.nth(count.saturating_sub(1))
.map_or(0, |(at, _)| at);
&text[at..]
}
fn head(text: &str, count: usize) -> &str {
let at = text
.char_indices()
.nth(count)
.map_or(text.len(), |(at, _)| at);
&text[..at]
}
fn common_tail(a: &str, b: &str) -> usize {
a.chars()
.rev()
.zip(b.chars().rev())
.take_while(|(a, b)| a == b)
.count()
}
fn common_head(a: &str, b: &str) -> usize {
a.chars().zip(b.chars()).take_while(|(a, b)| a == b).count()
}