use ratatui::style::Modifier;
use ratatui::text::{Line, Span};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SearchKind {
FileFind,
Grep,
}
impl SearchKind {
pub fn title(self) -> &'static str {
match self {
SearchKind::FileFind => " find file ",
SearchKind::Grep => " grep ",
}
}
pub fn slug(self) -> &'static str {
match self {
SearchKind::FileFind => "find",
SearchKind::Grep => "grep",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Scope {
Repo,
Org,
Global,
}
impl Scope {
pub fn stored(self) -> &'static str {
match self {
Scope::Repo => "repo",
Scope::Org => "org",
Scope::Global => "global",
}
}
pub fn from_stored(s: &str) -> Option<Scope> {
match s {
"repo" => Some(Scope::Repo),
"org" => Some(Scope::Org),
"global" => Some(Scope::Global),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RawHit {
pub repo: String,
pub path: String,
pub sha: String,
pub branch: String,
pub line: u32,
pub preview: Vec<(u32, String)>,
pub match_count: u32,
pub stale: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchHit {
pub repo: String,
pub path: String,
pub sha: String,
pub branch: String,
pub line: u32,
pub preview: Vec<(u32, Line<'static>)>,
pub match_count: u32,
pub body: String,
pub stale: bool,
pub unlocatable: bool,
}
impl SearchHit {
pub fn plain(
repo: &str,
path: &str,
line: u32,
preview: Vec<(u32, String)>,
match_count: u32,
body: String,
) -> Self {
SearchHit {
repo: repo.into(),
path: path.into(),
sha: String::new(),
branch: "main".into(),
line,
preview: preview
.into_iter()
.map(|(no, text)| (no, Line::from(Span::raw(text))))
.collect(),
match_count,
body,
stale: false,
unlocatable: false,
}
}
pub fn from_raw(raw: RawHit) -> Self {
let mut hit = SearchHit::plain(
&raw.repo,
&raw.path,
raw.line,
raw.preview,
raw.match_count,
String::new(),
);
hit.sha = raw.sha;
hit.branch = raw.branch;
hit.stale = raw.stale;
hit
}
pub fn preview_text(&self) -> String {
self.preview
.iter()
.map(|(_, line)| {
line.spans
.iter()
.map(|s| s.content.as_ref())
.collect::<String>()
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn with_highlighted(self, lines: Vec<Line<'static>>) -> Self {
let preview = self
.preview
.into_iter()
.zip(lines)
.map(|((no, _), line)| (no, line))
.collect();
SearchHit { preview, ..self }
}
}
pub(super) fn line_text(line: &Line) -> String {
line.spans.iter().map(|s| s.content.as_ref()).collect()
}
pub fn highlight_matches(
hits: &mut [SearchHit],
query: &str,
match_bg: ratatui::style::Color,
match_fg: ratatui::style::Color,
) {
let needle = query.to_lowercase();
if needle.is_empty() {
return;
}
let chip = ratatui::style::Style::default()
.fg(match_fg)
.bg(match_bg)
.add_modifier(Modifier::BOLD);
for hit in hits {
for (_, line) in &mut hit.preview {
let mut spans: Vec<Span<'static>> = Vec::new();
for span in &line.spans {
let text = span.content.to_string();
let lower = text.to_lowercase();
let mut at = 0; let mut rest = lower.as_str();
while let Some(pos) = rest.find(&needle) {
let (start, end) = (at + pos, at + pos + needle.len());
if start > at {
spans.push(Span::styled(text[at..start].to_string(), span.style));
}
spans.push(Span::styled(text[start..end].to_string(), chip));
at = end;
rest = &lower[end..];
}
if at < text.len() {
spans.push(Span::styled(text[at..].to_string(), span.style));
}
}
*line = Line::from(spans);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::global_search::mock;
use ratatui::style::Color;
#[test]
fn highlight_matches_chips_query_spans() {
let mut hits = mock::hits(SearchKind::Grep, "query", "");
highlight_matches(&mut hits, "query", Color::Yellow, Color::Black);
let hit = hits.iter().find(|h| h.path == "src/terminal.rs").unwrap();
let line = &hit.preview[2].1;
let chipped: Vec<&Span> = line
.spans
.iter()
.filter(|s| s.style.bg == Some(Color::Yellow))
.collect();
assert_eq!(chipped.len(), 2);
assert!(chipped.iter().all(|s| s.content.as_ref() == "query"));
}
}