use std::borrow::Cow;
use std::collections::HashSet;
use std::sync::Arc;
use skim::prelude::{unbounded, ExactOrFuzzyEngineFactory, Key, SkimOptionsBuilder};
use skim::{
AnsiString, DisplayContext, FuzzyAlgorithm, ItemPreview, MatchEngineFactory, MatchRange,
Matches, PreviewContext, Skim, SkimItem, SkimItemReceiver, SkimItemSender,
};
use syntect::highlighting::Style;
use crate::errors::LostTheWay;
use crate::language::Language;
use crate::the_way::{snippet::Snippet, TheWay};
use crate::utils;
#[derive(Debug)]
struct SearchSnippet {
index: usize,
text_highlight: String,
code: SearchCode,
}
#[derive(Debug, Clone)]
struct SearchCode {
code_fragments: Vec<(Style, String)>,
selection_style: Style,
code_highlight: String,
exact: bool,
}
impl SkimItem for SearchCode {
fn text(&self) -> Cow<str> {
AnsiString::parse(&self.code_highlight).into_inner()
}
}
impl<'a> SkimItem for SearchSnippet {
fn text(&self) -> Cow<str> {
AnsiString::parse(&self.text_highlight).into_inner()
+ AnsiString::parse(&self.code.code_highlight).into_inner()
}
fn display<'b>(&'b self, context: DisplayContext<'b>) -> AnsiString<'b> {
let mut text = AnsiString::parse(&self.text_highlight);
match context.matches {
Matches::CharIndices(indices) => {
text.override_attrs(
indices
.iter()
.filter(|&i| *i < self.text_highlight.len() - 1)
.map(|i| (context.highlight_attr, (*i as u32, (*i + 1) as u32)))
.collect(),
);
}
Matches::CharRange(start, end) => {
if end < self.text_highlight.len() {
text.override_attrs(vec![(context.highlight_attr, (start as u32, end as u32))]);
}
}
Matches::ByteRange(start, end) => {
if end < text.stripped().len() {
let start = text.stripped()[..start].chars().count();
let end = start + text.stripped()[start..end].chars().count();
text.override_attrs(vec![(context.highlight_attr, (start as u32, end as u32))]);
}
}
Matches::None => (),
}
text
}
fn preview(&self, context: PreviewContext) -> ItemPreview {
if context.selected_indices.contains(&context.current_index) {
let fuzzy_engine = ExactOrFuzzyEngineFactory::builder()
.exact_mode(self.code.exact)
.fuzzy_algorithm(FuzzyAlgorithm::SkimV2)
.build()
.create_engine(context.query);
if let Some(match_result) = fuzzy_engine.match_item(Arc::new(self.code.clone())) {
let indices: HashSet<_> = match match_result.matched_range {
MatchRange::ByteRange(start, end) => (start..end).collect(),
MatchRange::Chars(indices) => indices.into_iter().collect(),
};
ItemPreview::AnsiText(
self.code
.code_fragments
.iter()
.flat_map(|(style, line)| {
line.chars().map(move |c| (*style, c.to_string()))
})
.enumerate()
.map(|(i, (style, line))| {
if indices.contains(&i) {
utils::highlight_strings(&[(self.code.selection_style, line)], true)
} else {
utils::highlight_string(&line, style)
}
})
.collect::<Vec<_>>()
.join(""),
)
} else {
ItemPreview::AnsiText(self.code.code_highlight.clone())
}
} else {
ItemPreview::AnsiText(self.code.code_highlight.clone())
}
}
}
impl TheWay {
pub(crate) fn make_search(
&mut self,
snippets: Vec<Snippet>,
skim_theme: String,
selection_style: Style,
stdout: bool,
exact: bool,
) -> color_eyre::Result<()> {
let default_language = Language::default();
let search_snippets: Vec<_> = snippets
.into_iter()
.map(|snippet| {
let language = self
.languages
.get(&snippet.language)
.unwrap_or(&default_language);
let code_fragments = self
.highlighter
.highlight_code(&snippet.code, &snippet.extension);
let code_highlight = utils::highlight_strings(&code_fragments, false);
SearchSnippet {
code: SearchCode {
code_fragments,
selection_style,
code_highlight,
exact,
},
text_highlight: utils::highlight_strings(
&snippet.pretty_print_header(&self.highlighter, language),
false,
),
index: snippet.index,
}
})
.collect();
let options = SkimOptionsBuilder::default()
.height(Some("100%"))
.preview(Some(""))
.preview_window(Some("up:70%:wrap"))
.bind(vec![
"shift-left:accept",
"shift-right:accept",
"Enter:accept",
])
.header(Some(
"Press Enter to copy, Shift-left to delete, Shift-right to edit",
))
.exact(exact)
.multi(true)
.reverse(true)
.color(Some(&skim_theme))
.build()
.map_err(|_| LostTheWay::SearchError)?;
let (tx_item, rx_item): (SkimItemSender, SkimItemReceiver) = unbounded();
for item in search_snippets {
let _ = tx_item.send(Arc::new(item));
}
drop(tx_item);
if let Some(output) = Skim::run_with(&options, Some(rx_item)) {
let key = output.final_key;
for item in &output.selected_items {
let snippet: &SearchSnippet =
(*item).as_any().downcast_ref::<SearchSnippet>().unwrap();
match key {
Key::Enter => {
self.copy(snippet.index, stdout)?;
}
Key::ShiftLeft => {
self.delete(snippet.index, false)?;
}
Key::ShiftRight => {
self.edit(snippet.index)?;
}
_ => (),
}
}
}
Ok(())
}
}