//! `dev.knotless.searchCode`: the code of public repositories, as Zoekt //! finds it ([`crate::code`]). use super::*; use serde_json::{Value, json}; /// How many files a search shows, if not asked, and at most. const FILES: usize = 20; const FILES_MAX: usize = 50; /// How deep the pages of a search go: each page asks Zoekt for all of /// them, as it cuts off files it scores alike as they come. const DEEPEST: usize = 200; #[derive(Deserialize)] pub struct SearchParams { q: String, repo: Option, limit: Option, cursor: Option, } /// A file found, in the repository the index lists it in. #[derive(Serialize)] struct FileView { repo: Value, path: String, #[serde(skip_serializing_if = "String::is_empty")] language: String, #[serde(skip_serializing_if = "String::is_empty")] commit: String, chunks: Vec, } /// A language of the files found, and how many there are. #[derive(Serialize)] struct LanguageCount { name: String, #[serde(skip_serializing_if = "Option::is_none")] color: Option<&'static str>, files: u64, } /// `dev.knotless.searchCode`: what `q` finds in the default branches of /// the public repositories this server indexes, or of the one `repo` /// names; none of a repository the index no longer lists, or has taken /// down. With them, the languages of all it finds, its `lang:` filters /// left out, so each can be picked. pub async fn search_code( State(app): State>, Query(p): Query, ) -> Result { let (Some(code), Some(index)) = (app.code.as_deref(), app.index.as_deref()) else { return Err(Shown( StatusCode::NOT_IMPLEMENTED, "this server searches no code".into(), )); }; let mut within = None; if let Some(uri) = &p.repo { let repo = repo::found(&app, uri).await?; within = Some((repo.did, repo.key)); } let within = within .as_ref() .map(|(did, key)| (did.as_str(), key.as_str())); let limit = p.limit.unwrap_or(FILES).clamp(1, FILES_MAX); let skip = repo::skipped(p.cursor.as_deref())?; let upto = (skip + limit).min(DEEPEST); // The languages are secondary to what was found: a query Zoekt cannot // count them by lists none, rather than failing the search. The first // page alone lists them, as the pages after add only files. let faceted = async { match skip { 0 => code.faceted(&p.q, within).await.unwrap_or_default(), _ => Vec::new(), } }; let (found, faceted) = tokio::join!(code.search(&p.q, within, DEEPEST), faceted); let found = found?; let more = upto < DEEPEST && found.file_count > upto as u64; let listed = |h: crate::code::Hit| Some((index.listed(&h.did, &h.key).ok().flatten()?, h)); let pages_before = found.hits.into_iter().take(upto).skip(skip); let shown: Vec<_> = pages_before.filter_map(listed).collect(); let owners = shown.iter().map(|(r, _)| actor(&app, &r.did)); let owners = futures::future::join_all(owners).await; let files: Vec = shown .into_iter() .zip(owners) .map(|((r, hit), owner)| { let mut repo = json!(r); repo["owner"] = json!(owner); FileView { repo, path: hit.path, language: hit.language, commit: hit.commit, chunks: hit.chunks, } }) .collect(); // Each repository asked after once, however many of its files count. let mut shown = std::collections::HashMap::new(); let mut counted = std::collections::BTreeMap::::new(); for hit in faceted.into_iter().filter(|h| !h.language.is_empty()) { let repo = (hit.did, hit.key); let listed = |(did, key): &(String, String)| index.listed(did, key).ok().flatten().is_some(); let listed = *shown.entry(repo.clone()).or_insert_with(|| listed(&repo)); *counted.entry(hit.language).or_default() += u64::from(listed); } // Most files first, then by name. let mut ranked: Vec<(std::cmp::Reverse, String)> = counted .into_iter() .filter(|(_, n)| *n > 0) .map(|(name, n)| (std::cmp::Reverse(n), name)) .collect(); ranked.sort(); let languages: Vec<_> = ranked .into_iter() .map(|(std::cmp::Reverse(files), name)| LanguageCount { color: crate::languages::color(&name), name, files, }) .collect(); let mut answered = json!({ "files": files, "fileCount": found.file_count, "matchCount": found.match_count, "languages": languages, }); if more { answered["cursor"] = json!(upto.to_string()); } Ok(answer(answered)) }