use crate::base::Rect;
use crate::render::{Attrs, Style};
use crate::text;
use crate::theme::TokenSet;
use crate::ui::StyledCanvas;
use super::Row;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct MdSearchMatch {
pub row: usize,
pub bytes: (usize, usize),
pub cells: (i32, i32),
}
pub(crate) fn find_in_rows(rows: &[Row], query: &str, fold_case: bool) -> Vec<MdSearchMatch> {
if query.is_empty() {
return Vec::new();
}
let needle = if fold_case {
fold(query)
} else {
query.to_string()
};
if needle.is_empty() {
return Vec::new();
}
let mut out = Vec::new();
for (row_idx, row) in rows.iter().enumerate() {
if row.rule || row.image.is_some() || row.line.is_empty() {
continue;
}
let plain = row.line.plain();
if fold_case {
let (folded, map) = fold_with_map(&plain);
let mut from = 0;
while let Some(rel) = folded[from..].find(needle.as_str()) {
let fa = from + rel;
let fb = fa + needle.len();
let a = map[fa].0;
let b = map[fb - 1].1;
push_match(&mut out, row_idx, row, &plain, a, b);
from = fb;
}
} else {
let mut from = 0;
while let Some(rel) = plain[from..].find(needle.as_str()) {
let a = from + rel;
let b = a + needle.len();
push_match(&mut out, row_idx, row, &plain, a, b);
from = b;
}
}
}
out
}
fn push_match(
out: &mut Vec<MdSearchMatch>,
row_idx: usize,
row: &Row,
plain: &str,
a: usize,
b: usize,
) {
let (a, b) = snap_to_clusters(plain, a, b);
out.push(MdSearchMatch {
row: row_idx,
bytes: (a, b),
cells: (row_col_at_byte(row, a), row_col_at_byte(row, b)),
});
}
pub(crate) fn draw_highlights(
canvas: &mut dyn StyledCanvas,
rect: Rect,
t: &TokenSet,
rows: &[Row],
offset: usize,
matches: &[MdSearchMatch],
current: Option<usize>,
) {
let base = Style::new().fg(t.selection_fg).bg(t.selection_bg);
let strong = base.attrs(Attrs::BOLD | Attrs::UNDERLINE);
let visible = rect.h.max(0) as usize;
for (i, m) in matches.iter().enumerate() {
if m.row < offset || m.row >= offset + visible {
continue;
}
let Some(row) = rows.get(m.row) else { continue };
let y = rect.y + (m.row - offset) as i32;
let style = if current == Some(i) { strong } else { base };
let mut span_start = 0usize;
let mut x = rect.x + row.indent;
for span in &row.line.spans {
let span_end = span_start + span.text.len();
if span_end > m.bytes.0 && span_start < m.bytes.1 {
let lo = m.bytes.0.max(span_start);
let hi = m.bytes.1.min(span_end);
let before = &span.text[..lo - span_start];
let slice = &span.text[lo - span_start..hi - span_start];
let sx = x + text::width(before);
super::super::richtext::print_span_clipped(
canvas,
sx,
y,
rect.right(),
slice,
&style,
);
}
x += text::width(&span.text);
span_start = span_end;
if span_start >= m.bytes.1 {
break;
}
}
}
}
pub(crate) fn row_col_at_byte(row: &Row, byte: usize) -> i32 {
let mut col = row.indent;
let mut base = 0usize;
for span in &row.line.spans {
for seg in text::segments(&span.text) {
if base + seg.offset + seg.cluster.len() > byte {
return col;
}
col += seg.width;
}
base += span.text.len();
}
col
}
#[allow(dead_code)] pub(crate) fn row_byte_at_col(row: &Row, col: i32) -> usize {
let mut cur = row.indent;
let mut base = 0usize;
for span in &row.line.spans {
for seg in text::segments(&span.text) {
if seg.width > 0 && col < cur + seg.width {
return base + seg.offset;
}
cur += seg.width;
}
base += span.text.len();
}
base
}
fn snap_to_clusters(plain: &str, a: usize, b: usize) -> (usize, usize) {
let mut start = 0usize;
let mut end = plain.len();
for seg in text::segments(plain) {
let s = seg.offset;
let e = seg.offset + seg.cluster.len();
if s <= a {
start = s;
}
if s < b && e >= b {
end = e;
break;
}
}
if b >= plain.len() {
end = plain.len();
}
(start.min(plain.len()), end.max(start))
}
fn fold(s: &str) -> String {
s.chars().flat_map(char::to_lowercase).collect()
}
fn fold_with_map(s: &str) -> (String, Vec<(usize, usize)>) {
let mut folded = String::with_capacity(s.len());
let mut map: Vec<(usize, usize)> = Vec::with_capacity(s.len());
for (o, c) in s.char_indices() {
let end = o + c.len_utf8();
for lc in c.to_lowercase() {
let n = lc.len_utf8();
folded.push(lc);
for _ in 0..n {
map.push((o, end));
}
}
}
(folded, map)
}
#[cfg(test)]
#[path = "markdown_search_tests.rs"]
mod tests;