mod results;
mod pane;
use pane::ExpandedFile;
pub use pane::YankTarget;
use self::model::line_text;
use super::preview::{Preview, PreviewContent};
use super::vim_input::{SubMode, VimInput};
use crate::action::Action;
use crate::mode::Mode;
use ratatui::crossterm::cursor::SetCursorStyle;
mod backend;
mod facets;
mod grammar;
mod keys;
pub mod mock;
mod model;
mod render;
use facets::FacetId;
pub use backend::run_view_search;
pub use model::{RawHit, Scope, SearchHit, SearchKind, chip_line, highlight_matches};
pub(crate) use backend::locate_in_blob;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Focus {
Query,
Scope,
Extension,
Facets,
Error,
Results,
}
const FOCUS_ORDER: [Focus; 6] = [
Focus::Query,
Focus::Scope,
Focus::Extension,
Focus::Facets,
Focus::Error,
Focus::Results,
];
pub struct GlobalSearch {
kind: SearchKind,
repo: Option<String>,
org: Option<String>,
pub query: VimInput,
extension: VimInput,
scope: Scope,
focus: Focus,
scope_popup: bool,
scope_cursor: usize,
scope_pre_popup: Scope,
hits: Vec<SearchHit>,
filter: VimInput,
filtering: bool,
pre_filter: String,
filter_value: String,
selected: usize,
scroll: u16,
load: crate::request::LoadState<crate::request::ContentSearchRequest>,
failure_preview: Option<Preview>,
clipped: bool,
facet: Option<FacetId>,
facet_cursor: usize,
dropped: usize,
index_as_of: Option<String>,
expanded: Option<ExpandedFile>,
blame: Option<crate::components::browser::BlameState>,
find_input: VimInput,
finding: bool,
client_filtered: usize,
unfiltered: Vec<String>,
pub search_ref_note: Option<String>,
}
const RENDER_CAP: usize = rootle_provider::RENDER_BUDGET;
impl GlobalSearch {
pub fn new(
kind: SearchKind,
repo: Option<String>,
org: Option<String>,
persisted_scope: Option<Scope>,
persisted_extension: Option<String>,
) -> Self {
let waterfall = if repo.is_some() {
Scope::Repo
} else if org.is_some() {
Scope::Org
} else {
Scope::Global
};
let enabled = |s: Scope| match s {
Scope::Repo => repo.is_some(),
Scope::Org => org.is_some(),
Scope::Global => true,
};
let scope = persisted_scope.filter(|s| enabled(*s)).unwrap_or(waterfall);
let mut extension = VimInput::new();
if let Some(ext) = persisted_extension.filter(|e| !e.is_empty()) {
extension.prefill(&ext); }
GlobalSearch {
kind,
scope,
repo,
org,
query: VimInput::new(),
extension,
focus: Focus::Query,
scope_popup: false,
scope_cursor: 0,
scope_pre_popup: Scope::Global,
hits: Vec::new(),
filter: VimInput::transient(),
clipped: false,
dropped: 0,
index_as_of: None,
client_filtered: 0,
unfiltered: vec![],
search_ref_note: None,
blame: None,
filtering: false,
pre_filter: String::new(),
filter_value: String::new(),
facet: None,
facet_cursor: 0,
selected: 0,
scroll: 0,
load: Default::default(),
failure_preview: None,
expanded: None,
find_input: VimInput::transient(),
finding: false,
}
}
pub fn kind(&self) -> SearchKind {
self.kind
}
pub fn scope(&self) -> Scope {
self.scope
}
pub fn extension_value(&self) -> String {
self.extension.value()
}
fn scope_items(&self) -> [(Scope, bool); 3] {
[
(Scope::Repo, self.repo.is_some()),
(Scope::Org, self.org.is_some()),
(Scope::Global, true),
]
}
fn scope_label(&self) -> String {
match self.scope {
Scope::Repo => match &self.repo {
Some(repo) => format!("repo:{repo}"),
None => "repo: —".into(),
},
Scope::Org => match &self.org {
Some(org) => format!("org:{org}"),
None => "org: —".into(),
},
Scope::Global => "global".into(),
}
}
pub fn context(&self) -> String {
let what = match self.kind {
SearchKind::FileFind => "find",
SearchKind::Grep => "grep",
};
let scope = self
.submitted_request()
.map(|request| request.scope.as_str());
let mut ctx = format!(
"{what} · {}",
scope
.map(str::to_string)
.unwrap_or_else(|| self.scope_label())
);
let extension = self
.submitted_request()
.map(|request| request.extension.clone())
.unwrap_or_else(|| self.extension.value());
if !extension.is_empty() {
ctx.push_str(&format!(" · ext:{extension}"));
}
ctx
}
pub fn effective_mode(&self) -> Mode {
if self.finding {
return Mode::Find;
}
if self.filtering {
return Mode::Search;
}
match self.focus {
Focus::Query => match self.query.submode {
SubMode::Insert => Mode::Insert,
SubMode::Normal => Mode::Normal,
},
Focus::Extension => match self.extension.submode {
SubMode::Insert => Mode::Insert,
SubMode::Normal => Mode::Normal,
},
Focus::Scope | Focus::Facets | Focus::Results | Focus::Error => Mode::Browse,
}
}
pub fn cursor_style(&self) -> Option<SetCursorStyle> {
let input = match self.focus {
Focus::Query => &self.query,
Focus::Extension => &self.extension,
_ => return None,
};
Some(match input.submode {
SubMode::Insert => SetCursorStyle::SteadyBar,
SubMode::Normal => SetCursorStyle::SteadyBlock,
})
}
pub fn submitted_request(&self) -> Option<&crate::request::ContentSearchRequest> {
self.load.request.as_ref()
}
pub fn observation(&self) -> serde_json::Value {
serde_json::json!({
"kind": self.kind,
"phase": self.load.phase,
"request": self.load.request,
"retained_hit_count": self.hits.len(),
"visible_hit_count": self.visible().len(),
"truncated": if self.clipped { Some(true) } else if self.load.phase == crate::request::LoadPhase::Ready { Some(false) } else { None },
"error": self.load.error,
})
}
pub(crate) fn diagnostics(&self) -> SearchDiagnostics {
SearchDiagnostics {
kind: self.kind.slug(),
scope: self.scope.stored(),
focus: match self.focus {
Focus::Query => "query",
Focus::Scope => "scope",
Focus::Extension => "extension",
Focus::Facets => "facets",
Focus::Error => "error",
Focus::Results => "results",
},
selected: self.selected,
hits: self.hits.len(),
dropped: self.dropped,
clipped: self.clipped,
pending: self.load.phase == crate::request::LoadPhase::Loading,
filtering: self.filtering,
finding: self.finding,
expanded: self.expanded.is_some(),
query_len: self.query.value().len(),
filter_len: self.filter_value.len(),
error_len: self.load.error.as_ref().map(|error| error.message.len()),
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct SearchDiagnostics {
pub(crate) kind: &'static str,
pub(crate) scope: &'static str,
pub(crate) focus: &'static str,
pub(crate) selected: usize,
pub(crate) hits: usize,
pub(crate) dropped: usize,
pub(crate) clipped: bool,
pub(crate) pending: bool,
pub(crate) filtering: bool,
pub(crate) finding: bool,
pub(crate) expanded: bool,
pub(crate) query_len: usize,
pub(crate) filter_len: usize,
pub(crate) error_len: Option<usize>,
}
fn file_title(hit: &SearchHit) -> String {
let repo = crate::sanitize::sanitize_inline(&hit.repo);
let path = crate::sanitize::sanitize_inline(&hit.path);
if hit.line > 0 {
format!("{repo}/{path}:{}", hit.line)
} else {
format!("{repo}/{path}")
}
}
#[cfg(test)]
mod tests;