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,
Results,
}
const FOCUS_ORDER: [Focus; 5] = [
Focus::Query,
Focus::Scope,
Focus::Extension,
Focus::Facets,
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,
pending: bool,
error: Option<String>,
submitted_once: bool,
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,
pending: false,
error: None,
submitted_once: false,
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 mut ctx = format!("{what} · {}", self.scope_label());
if !self.extension.value().is_empty() {
ctx.push_str(&format!(" · ext:{}", self.extension.value()));
}
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 => 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,
})
}
}
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;